mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-28 20:17:51 +02:00
Upstream merge
Internal permission change Fix url adder
This commit is contained in:
29
app/src/main/java/exh/EHSourceHelpers.kt
Executable file
29
app/src/main/java/exh/EHSourceHelpers.kt
Executable file
@ -0,0 +1,29 @@
|
||||
package exh
|
||||
|
||||
/**
|
||||
* Source helpers
|
||||
*/
|
||||
|
||||
val LEWD_SOURCE_SERIES = 6900L
|
||||
val EH_SOURCE_ID = LEWD_SOURCE_SERIES + 1
|
||||
val EXH_SOURCE_ID = LEWD_SOURCE_SERIES + 2
|
||||
val EH_METADATA_SOURCE_ID = LEWD_SOURCE_SERIES + 3
|
||||
val EXH_METADATA_SOURCE_ID = LEWD_SOURCE_SERIES + 4
|
||||
|
||||
val PERV_EDEN_EN_SOURCE_ID = LEWD_SOURCE_SERIES + 5
|
||||
val PERV_EDEN_IT_SOURCE_ID = LEWD_SOURCE_SERIES + 6
|
||||
|
||||
val NHENTAI_SOURCE_ID = LEWD_SOURCE_SERIES + 7
|
||||
|
||||
fun isLewdSource(source: Long) = source in 6900..6999
|
||||
|
||||
fun isEhSource(source: Long) = source == EH_SOURCE_ID
|
||||
|| source == EH_METADATA_SOURCE_ID
|
||||
|
||||
fun isExSource(source: Long) = source == EXH_SOURCE_ID
|
||||
|| source == EXH_METADATA_SOURCE_ID
|
||||
|
||||
fun isPervEdenSource(source: Long) = source == PERV_EDEN_IT_SOURCE_ID
|
||||
|| source == PERV_EDEN_EN_SOURCE_ID
|
||||
|
||||
fun isNhentaiSource(source: Long) = source == NHENTAI_SOURCE_ID
|
135
app/src/main/java/exh/FavoritesSyncHelper.kt
Executable file
135
app/src/main/java/exh/FavoritesSyncHelper.kt
Executable file
@ -0,0 +1,135 @@
|
||||
package exh
|
||||
|
||||
import android.app.Activity
|
||||
import android.support.v7.app.AlertDialog
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.online.all.EHentai
|
||||
import eu.kanade.tachiyomi.util.syncChaptersWithSource
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
class FavoritesSyncHelper(val activity: Activity) {
|
||||
|
||||
val db: DatabaseHelper by injectLazy()
|
||||
|
||||
val sourceManager: SourceManager by injectLazy()
|
||||
|
||||
val prefs: PreferencesHelper by injectLazy()
|
||||
|
||||
fun guiSyncFavorites(onComplete: () -> Unit) {
|
||||
//ExHentai must be enabled/user must be logged in
|
||||
if (!prefs.enableExhentai().getOrDefault()) {
|
||||
AlertDialog.Builder(activity).setTitle("Error")
|
||||
.setMessage("You are not logged in! Please log in and try again!")
|
||||
.setPositiveButton("Ok") { dialog, _ -> dialog.dismiss() }.show()
|
||||
return
|
||||
}
|
||||
val dialog = MaterialDialog.Builder(activity)
|
||||
.progress(true, 0)
|
||||
.title("Downloading favorites")
|
||||
.content("Please wait...")
|
||||
.cancelable(false)
|
||||
.show()
|
||||
thread {
|
||||
var error = false
|
||||
try {
|
||||
syncFavorites()
|
||||
} catch (e: Exception) {
|
||||
error = true
|
||||
Timber.e(e, "Could not sync favorites!")
|
||||
}
|
||||
|
||||
dialog.dismiss()
|
||||
|
||||
activity.runOnUiThread {
|
||||
if (error)
|
||||
MaterialDialog.Builder(activity)
|
||||
.title("Error")
|
||||
.content("There was an error downloading your favorites, please try again later!")
|
||||
.positiveText("Ok")
|
||||
.show()
|
||||
onComplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun syncFavorites() {
|
||||
val onlineSources = sourceManager.getOnlineSources()
|
||||
var ehSource: EHentai? = null
|
||||
var exSource: EHentai? = null
|
||||
onlineSources.forEach {
|
||||
if(it.id == EH_SOURCE_ID)
|
||||
ehSource = it as EHentai
|
||||
else if(it.id == EXH_SOURCE_ID)
|
||||
exSource = it as EHentai
|
||||
}
|
||||
|
||||
(exSource ?: ehSource)?.let { source ->
|
||||
val favResponse = source.fetchFavorites()
|
||||
val ourCategories = ArrayList<Category>(db.getCategories().executeAsBlocking())
|
||||
val ourMangas = ArrayList<Manga>(db.getMangas().executeAsBlocking())
|
||||
//Add required categories (categories do not sync upwards)
|
||||
favResponse.second.filter { theirCategory ->
|
||||
ourCategories.find {
|
||||
it.name.endsWith(theirCategory)
|
||||
} == null
|
||||
}.map {
|
||||
Category.create(it)
|
||||
}.let {
|
||||
db.inTransaction {
|
||||
//Insert new categories
|
||||
db.insertCategories(it).executeAsBlocking().results().entries.filter {
|
||||
it.value.wasInserted()
|
||||
}.forEach { it.key.id = it.value.insertedId()!!.toInt() }
|
||||
|
||||
val categoryMap = (it + ourCategories).associateBy { it.name }
|
||||
|
||||
//Insert new mangas
|
||||
val mangaToInsert = java.util.ArrayList<Manga>()
|
||||
favResponse.first.map {
|
||||
val category = categoryMap[it.fav]!!
|
||||
var manga = it.manga
|
||||
val alreadyHaveManga = ourMangas.find {
|
||||
it.url == manga.url
|
||||
}?.apply {
|
||||
manga = this
|
||||
} != null
|
||||
if (!alreadyHaveManga) {
|
||||
ourMangas.add(manga)
|
||||
mangaToInsert.add(manga)
|
||||
}
|
||||
manga.favorite = true
|
||||
Pair(manga, category)
|
||||
}.apply {
|
||||
//Insert mangas
|
||||
db.insertMangas(mangaToInsert).executeAsBlocking().results().entries.filter {
|
||||
it.value.wasInserted()
|
||||
}.forEach { manga ->
|
||||
manga.key.id = manga.value.insertedId()
|
||||
try {
|
||||
source.fetchChapterList(manga.key).map {
|
||||
syncChaptersWithSource(db, it, manga.key, source)
|
||||
}.toBlocking().first()
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e, "Failed to update chapters for gallery: ${manga.key.title}!")
|
||||
}
|
||||
}
|
||||
|
||||
//Set categories
|
||||
val categories = map { MangaCategory.create(it.first, it.second) }
|
||||
val mangas = map { it.first }
|
||||
db.setMangaCategories(categories, mangas)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
192
app/src/main/java/exh/FavoritesSyncManager.java
Executable file
192
app/src/main/java/exh/FavoritesSyncManager.java
Executable file
@ -0,0 +1,192 @@
|
||||
package exh;
|
||||
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper;
|
||||
import eu.kanade.tachiyomi.data.database.models.Category;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory;
|
||||
import eu.kanade.tachiyomi.source.online.all.EHentai;
|
||||
import kotlin.Pair;
|
||||
//import eu.kanade.tachiyomi.data.source.online.english.EHentai;
|
||||
|
||||
public class FavoritesSyncManager {
|
||||
/*Context context;
|
||||
DatabaseHelper db;
|
||||
|
||||
public FavoritesSyncManager(Context context, DatabaseHelper db) {
|
||||
this.context = context;
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
public void guiSyncFavorites(final Runnable onComplete) {
|
||||
if(!DialogLogin.isLoggedIn(context, false)) {
|
||||
new AlertDialog.Builder(context).setTitle("Error")
|
||||
.setMessage("You are not logged in! Please log in and try again!")
|
||||
.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
}).show();
|
||||
return;
|
||||
}
|
||||
final ProgressDialog dialog = ProgressDialog.show(context, "Downloading Favorites", "Please wait...", true, false);
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Handler mainLooper = new Handler(Looper.getMainLooper());
|
||||
try {
|
||||
syncFavorites();
|
||||
} catch (Exception e) {
|
||||
mainLooper.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle("Error")
|
||||
.setMessage("There was an error downloading your favorites, please try again later!")
|
||||
.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
}).show();
|
||||
}
|
||||
});
|
||||
e.printStackTrace();
|
||||
}
|
||||
dialog.dismiss();
|
||||
mainLooper.post(onComplete);
|
||||
}
|
||||
}).start();
|
||||
}*/
|
||||
/*
|
||||
public void syncFavorites() throws IOException {
|
||||
Pair favResponse = EHentai.fetchFavorites(context);
|
||||
Map<String, List<Manga>> favorites = favResponse.favs;
|
||||
List<Category> ourCategories = new ArrayList<>(db.getCategories().executeAsBlocking());
|
||||
List<Manga> ourMangas = new ArrayList<>(db.getMangas().executeAsBlocking());
|
||||
//Add required categories (categories do not sync upwards)
|
||||
List<Category> categoriesToInsert = new ArrayList<>();
|
||||
for (String theirCategory : favorites.keySet()) {
|
||||
boolean haveCategory = false;
|
||||
for (Category category : ourCategories) {
|
||||
if (category.getName().endsWith(theirCategory)) {
|
||||
haveCategory = true;
|
||||
}
|
||||
}
|
||||
if (!haveCategory) {
|
||||
Category category = Category.Companion.create(theirCategory);
|
||||
ourCategories.add(category);
|
||||
categoriesToInsert.add(category);
|
||||
}
|
||||
}
|
||||
if (!categoriesToInsert.isEmpty()) {
|
||||
for(Map.Entry<Category, PutResult> result : db.insertCategories(categoriesToInsert).executeAsBlocking().results().entrySet()) {
|
||||
if(result.getValue().wasInserted()) {
|
||||
result.getKey().setId(result.getValue().insertedId().intValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
//Build category map
|
||||
Map<String, Category> categoryMap = new HashMap<>();
|
||||
for (Category category : ourCategories) {
|
||||
categoryMap.put(category.getName(), category);
|
||||
}
|
||||
//Insert new mangas
|
||||
List<Manga> mangaToInsert = new ArrayList<>();
|
||||
Map<Manga, Category> mangaToSetCategories = new HashMap<>();
|
||||
for (Map.Entry<String, List<Manga>> entry : favorites.entrySet()) {
|
||||
Category category = categoryMap.get(entry.getKey());
|
||||
for (Manga manga : entry.getValue()) {
|
||||
boolean alreadyHaveManga = false;
|
||||
for (Manga ourManga : ourMangas) {
|
||||
if (ourManga.getUrl().equals(manga.getUrl())) {
|
||||
alreadyHaveManga = true;
|
||||
manga = ourManga;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!alreadyHaveManga) {
|
||||
ourMangas.add(manga);
|
||||
mangaToInsert.add(manga);
|
||||
}
|
||||
mangaToSetCategories.put(manga, category);
|
||||
manga.setFavorite(true);
|
||||
}
|
||||
}
|
||||
for (Map.Entry<Manga, PutResult> results : db.insertMangas(mangaToInsert).executeAsBlocking().results().entrySet()) {
|
||||
if(results.getValue().wasInserted()) {
|
||||
results.getKey().setId(results.getValue().insertedId());
|
||||
}
|
||||
}
|
||||
for(Map.Entry<Manga, Category> entry : mangaToSetCategories.entrySet()) {
|
||||
db.setMangaCategories(Collections.singletonList(MangaCategory.Companion.create(entry.getKey(), entry.getValue())),
|
||||
Collections.singletonList(entry.getKey()));
|
||||
}*/
|
||||
//Determines what
|
||||
/*Map<Integer, List<Manga>> toUpload = new HashMap<>();
|
||||
for (Manga manga : ourMangas) {
|
||||
if(manga.getFavorite()) {
|
||||
boolean remoteHasManga = false;
|
||||
for (List<Manga> remoteMangas : favorites.values()) {
|
||||
for (Manga remoteManga : remoteMangas) {
|
||||
if (remoteManga.getUrl().equals(manga.getUrl())) {
|
||||
remoteHasManga = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!remoteHasManga) {
|
||||
List<Category> mangaCategories = db.getCategoriesForManga(manga).executeAsBlocking();
|
||||
for (Category category : mangaCategories) {
|
||||
int categoryIndex = favResponse.favCategories.indexOf(category.getName());
|
||||
if (categoryIndex >= 0) {
|
||||
List<Manga> uploadMangas = toUpload.get(categoryIndex);
|
||||
if (uploadMangas == null) {
|
||||
uploadMangas = new ArrayList<>();
|
||||
toUpload.put(categoryIndex, uploadMangas);
|
||||
}
|
||||
uploadMangas.add(manga);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
/********** NON-FUNCTIONAL, modifygids[] CANNOT ADD NEW FAVORITES! (or as of my testing it can't, maybe I'll do more testing)**/
|
||||
/*PreferencesHelper helper = new PreferencesHelper(context);
|
||||
for(Map.Entry<Integer, List<Manga>> entry : toUpload.entrySet()) {
|
||||
FormBody.Builder formBody = new FormBody.Builder()
|
||||
.add("ddact", "fav" + entry.getKey());
|
||||
for(Manga manga : entry.getValue()) {
|
||||
List<String> splitUrl = new ArrayList<>(Arrays.asList(manga.getUrl().split("/")));
|
||||
splitUrl.removeAll(Collections.singleton(""));
|
||||
if(splitUrl.size() < 2) {
|
||||
continue;
|
||||
}
|
||||
formBody.add("modifygids[]", splitUrl.get(1).trim());
|
||||
}
|
||||
formBody.add("apply", "Apply");
|
||||
Request request = RequestsKt.POST(EHentai.buildFavoritesBase(context, helper.getPrefs()).favoritesBase,
|
||||
EHentai.getHeadersBuilder(helper).build(),
|
||||
formBody.build(),
|
||||
RequestsKt.getDEFAULT_CACHE_CONTROL());
|
||||
Response response = NetworkManager.getInstance().getClient().newCall(request).execute();
|
||||
Util.d("EHentai", response.body().string());
|
||||
}*/
|
||||
// }
|
||||
}
|
80
app/src/main/java/exh/GalleryAdder.kt
Executable file
80
app/src/main/java/exh/GalleryAdder.kt
Executable file
@ -0,0 +1,80 @@
|
||||
package exh
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.util.syncChaptersWithSource
|
||||
import exh.metadata.MetadataHelper
|
||||
import exh.metadata.copyTo
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.net.MalformedURLException
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
import java.net.URL
|
||||
|
||||
class GalleryAdder {
|
||||
|
||||
private val db: DatabaseHelper by injectLazy()
|
||||
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
|
||||
private val metadataHelper = MetadataHelper()
|
||||
|
||||
fun addGallery(url: String, fav: Boolean = false): Manga {
|
||||
val source = when(URL(url).host) {
|
||||
"g.e-hentai.org", "e-hentai.org" -> EH_SOURCE_ID
|
||||
"exhentai.org" -> EXH_SOURCE_ID
|
||||
else -> throw MalformedURLException("Not a valid gallery URL!")
|
||||
}
|
||||
|
||||
val sourceObj = sourceManager.get(source)
|
||||
?: throw IllegalStateException("Could not find EH source!")
|
||||
|
||||
val pathOnlyUrl = getUrlWithoutDomain(url)
|
||||
|
||||
//Use manga in DB if possible, otherwise, make a new manga
|
||||
val manga = db.getManga(pathOnlyUrl, source).executeAsBlocking()
|
||||
?: Manga.create(source).apply {
|
||||
this.url = pathOnlyUrl
|
||||
title = url
|
||||
}
|
||||
|
||||
//Copy basics
|
||||
manga.copyFrom(sourceObj.fetchMangaDetails(manga).toBlocking().first())
|
||||
|
||||
//Apply metadata
|
||||
metadataHelper.fetchEhMetadata(url, isExSource(source))?.copyTo(manga)
|
||||
|
||||
if(fav) manga.favorite = true
|
||||
|
||||
db.insertManga(manga).executeAsBlocking().insertedId()?.let {
|
||||
manga.id = it
|
||||
}
|
||||
|
||||
//Fetch and copy chapters
|
||||
try {
|
||||
sourceObj.fetchChapterList(manga).map {
|
||||
syncChaptersWithSource(db, it, manga, sourceObj)
|
||||
}.toBlocking().first()
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e, "Failed to update chapters for gallery: ${manga.title}!")
|
||||
}
|
||||
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun getUrlWithoutDomain(orig: String): String {
|
||||
try {
|
||||
val uri = URI(orig)
|
||||
var out = uri.path
|
||||
if (uri.query != null)
|
||||
out += "?" + uri.query
|
||||
if (uri.fragment != null)
|
||||
out += "#" + uri.fragment
|
||||
return out
|
||||
} catch (e: URISyntaxException) {
|
||||
return orig
|
||||
}
|
||||
}
|
||||
}
|
3
app/src/main/java/exh/StringBuilderExtensions.kt
Executable file
3
app/src/main/java/exh/StringBuilderExtensions.kt
Executable file
@ -0,0 +1,3 @@
|
||||
package exh
|
||||
|
||||
operator fun StringBuilder.plusAssign(other: String) { append(other) }
|
5
app/src/main/java/exh/VerbelExpressionExtensions.kt
Executable file
5
app/src/main/java/exh/VerbelExpressionExtensions.kt
Executable file
@ -0,0 +1,5 @@
|
||||
package exh
|
||||
|
||||
import ru.lanwen.verbalregex.VerbalExpression
|
||||
|
||||
fun VerbalExpression.Builder.anyChar() = add(".")!!
|
62
app/src/main/java/exh/metadata/MetadataHelper.kt
Executable file
62
app/src/main/java/exh/metadata/MetadataHelper.kt
Executable file
@ -0,0 +1,62 @@
|
||||
package exh.metadata
|
||||
|
||||
import exh.*
|
||||
import exh.metadata.models.ExGalleryMetadata
|
||||
import exh.metadata.models.NHentaiMetadata
|
||||
import exh.metadata.models.PervEdenGalleryMetadata
|
||||
import exh.metadata.models.SearchableGalleryMetadata
|
||||
import io.paperdb.Paper
|
||||
|
||||
class MetadataHelper {
|
||||
|
||||
fun writeGallery(galleryMetadata: SearchableGalleryMetadata, source: Long)
|
||||
= (if(isExSource(source) || isEhSource(source)) exGalleryBook()
|
||||
else if(isPervEdenSource(source)) pervEdenGalleryBook()
|
||||
else if(isNhentaiSource(source)) nhentaiGalleryBook()
|
||||
else null)?.write(galleryMetadata.galleryUniqueIdentifier(), galleryMetadata)!!
|
||||
|
||||
fun fetchEhMetadata(url: String, exh: Boolean): ExGalleryMetadata?
|
||||
= ExGalleryMetadata().let {
|
||||
it.url = url
|
||||
it.exh = exh
|
||||
return exGalleryBook().read<ExGalleryMetadata>(it.galleryUniqueIdentifier())
|
||||
}
|
||||
|
||||
fun fetchPervEdenMetadata(url: String, source: Long): PervEdenGalleryMetadata?
|
||||
= PervEdenGalleryMetadata().let {
|
||||
it.url = url
|
||||
if(source == PERV_EDEN_EN_SOURCE_ID)
|
||||
it.lang = "en"
|
||||
else if(source == PERV_EDEN_IT_SOURCE_ID)
|
||||
it.lang = "it"
|
||||
else throw IllegalArgumentException("Invalid source id!")
|
||||
return pervEdenGalleryBook().read<PervEdenGalleryMetadata>(it.galleryUniqueIdentifier())
|
||||
}
|
||||
|
||||
fun fetchNhentaiMetadata(url: String) = NHentaiMetadata().let {
|
||||
it.url = url
|
||||
nhentaiGalleryBook().read<NHentaiMetadata>(it.galleryUniqueIdentifier())
|
||||
}
|
||||
|
||||
fun fetchMetadata(url: String, source: Long): SearchableGalleryMetadata? {
|
||||
if(isExSource(source) || isEhSource(source)) {
|
||||
return fetchEhMetadata(url, isExSource(source))
|
||||
} else if(isPervEdenSource(source)) {
|
||||
return fetchPervEdenMetadata(url, source)
|
||||
} else if(isNhentaiSource(source)) {
|
||||
return fetchNhentaiMetadata(url)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun getAllGalleries() = exGalleryBook().allKeys.map {
|
||||
exGalleryBook().read<ExGalleryMetadata>(it)
|
||||
}
|
||||
|
||||
fun exGalleryBook() = Paper.book("gallery-ex")!!
|
||||
|
||||
fun pervEdenGalleryBook() = Paper.book("gallery-perveden")!!
|
||||
|
||||
fun nhentaiGalleryBook() = Paper.book("gallery-nhentai")!!
|
||||
}
|
47
app/src/main/java/exh/metadata/MetadataUtil.kt
Executable file
47
app/src/main/java/exh/metadata/MetadataUtil.kt
Executable file
@ -0,0 +1,47 @@
|
||||
package exh.metadata
|
||||
|
||||
/**
|
||||
* Metadata utils
|
||||
*/
|
||||
fun humanReadableByteCount(bytes: Long, si: Boolean): String {
|
||||
val unit = if (si) 1000 else 1024
|
||||
if (bytes < unit) return bytes.toString() + " B"
|
||||
val exp = (Math.log(bytes.toDouble()) / Math.log(unit.toDouble())).toInt()
|
||||
val pre = (if (si) "kMGTPE" else "KMGTPE")[exp - 1] + if (si) "" else "i"
|
||||
return String.format("%.1f %sB", bytes / Math.pow(unit.toDouble(), exp.toDouble()), pre)
|
||||
}
|
||||
|
||||
private val KB_FACTOR: Long = 1000
|
||||
private val KIB_FACTOR: Long = 1024
|
||||
private val MB_FACTOR = 1000 * KB_FACTOR
|
||||
private val MIB_FACTOR = 1024 * KIB_FACTOR
|
||||
private val GB_FACTOR = 1000 * MB_FACTOR
|
||||
private val GIB_FACTOR = 1024 * MIB_FACTOR
|
||||
|
||||
fun parseHumanReadableByteCount(arg0: String): Double? {
|
||||
val spaceNdx = arg0.indexOf(" ")
|
||||
val ret = java.lang.Double.parseDouble(arg0.substring(0, spaceNdx))
|
||||
when (arg0.substring(spaceNdx + 1)) {
|
||||
"GB" -> return ret * GB_FACTOR
|
||||
"GiB" -> return ret * GIB_FACTOR
|
||||
"MB" -> return ret * MB_FACTOR
|
||||
"MiB" -> return ret * MIB_FACTOR
|
||||
"KB" -> return ret * KB_FACTOR
|
||||
"KiB" -> return ret * KIB_FACTOR
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
fun String?.nullIfBlank(): String? = if(isNullOrBlank())
|
||||
null
|
||||
else
|
||||
this
|
||||
|
||||
fun <T> ignore(expr: () -> T): T? {
|
||||
return try { expr() } catch (t: Throwable) { null }
|
||||
}
|
||||
|
||||
fun <K,V> Set<Map.Entry<K,V>>.forEach(action: (K, V) -> Unit) {
|
||||
forEach { action(it.key, it.value) }
|
||||
}
|
218
app/src/main/java/exh/metadata/MetdataCopier.kt
Executable file
218
app/src/main/java/exh/metadata/MetdataCopier.kt
Executable file
@ -0,0 +1,218 @@
|
||||
package exh.metadata
|
||||
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.all.EHentai
|
||||
import eu.kanade.tachiyomi.source.online.all.EHentaiMetadata
|
||||
import eu.kanade.tachiyomi.source.online.all.PervEden
|
||||
import exh.metadata.models.*
|
||||
import exh.plusAssign
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Copies gallery metadata to a manga object
|
||||
*/
|
||||
|
||||
private const val EH_ARTIST_NAMESPACE = "artist"
|
||||
private const val EH_AUTHOR_NAMESPACE = "author"
|
||||
|
||||
private const val NHENTAI_ARTIST_NAMESPACE = "artist"
|
||||
private const val NHENTAI_CATEGORIES_NAMESPACE = "category"
|
||||
|
||||
private val ONGOING_SUFFIX = arrayOf(
|
||||
"[ongoing]",
|
||||
"(ongoing)",
|
||||
"{ongoing}"
|
||||
)
|
||||
|
||||
val EX_DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US)
|
||||
|
||||
private val prefs: PreferencesHelper by injectLazy()
|
||||
|
||||
fun ExGalleryMetadata.copyTo(manga: SManga) {
|
||||
//TODO Find some way to do this with SManga
|
||||
/*exh?.let {
|
||||
manga.source = if(it)
|
||||
2
|
||||
else
|
||||
1
|
||||
}*/
|
||||
url?.let { manga.url = it }
|
||||
thumbnailUrl?.let { manga.thumbnail_url = it }
|
||||
|
||||
//No title bug?
|
||||
val titleObj = if(prefs.useJapaneseTitle().getOrDefault())
|
||||
altTitle ?: title
|
||||
else
|
||||
title
|
||||
titleObj?.let { manga.title = it }
|
||||
|
||||
//Set artist (if we can find one)
|
||||
tags[EH_ARTIST_NAMESPACE]?.let {
|
||||
if(it.isNotEmpty()) manga.artist = it.joinToString(transform = Tag::name)
|
||||
}
|
||||
//Set author (if we can find one)
|
||||
tags[EH_AUTHOR_NAMESPACE]?.let {
|
||||
if(it.isNotEmpty()) manga.author = it.joinToString(transform = Tag::name)
|
||||
}
|
||||
//Set genre
|
||||
genre?.let { manga.genre = it }
|
||||
|
||||
//Try to automatically identify if it is ongoing, we try not to be too lenient here to avoid making mistakes
|
||||
//We default to completed
|
||||
manga.status = SManga.COMPLETED
|
||||
title?.let { t ->
|
||||
ONGOING_SUFFIX.find {
|
||||
t.endsWith(it, ignoreCase = true)
|
||||
}?.let {
|
||||
manga.status = SManga.ONGOING
|
||||
}
|
||||
}
|
||||
|
||||
//Build a nice looking description out of what we know
|
||||
val titleDesc = StringBuilder()
|
||||
title?.let { titleDesc += "Title: $it\n" }
|
||||
altTitle?.let { titleDesc += "Alternate Title: $it\n" }
|
||||
|
||||
val detailsDesc = StringBuilder()
|
||||
uploader?.let { detailsDesc += "Uploader: $it\n" }
|
||||
datePosted?.let { detailsDesc += "Posted: ${EX_DATE_FORMAT.format(Date(it))}\n" }
|
||||
visible?.let { detailsDesc += "Visible: $it\n" }
|
||||
language?.let {
|
||||
detailsDesc += "Language: $it"
|
||||
if(translated == true) detailsDesc += " TR"
|
||||
detailsDesc += "\n"
|
||||
}
|
||||
size?.let { detailsDesc += "File Size: ${humanReadableByteCount(it, true)}\n" }
|
||||
length?.let { detailsDesc += "Length: $it pages\n" }
|
||||
favorites?.let { detailsDesc += "Favorited: $it times\n" }
|
||||
averageRating?.let {
|
||||
detailsDesc += "Rating: $it"
|
||||
ratingCount?.let { detailsDesc += " ($it)" }
|
||||
detailsDesc += "\n"
|
||||
}
|
||||
|
||||
val tagsDesc = buildTagsDescription(this)
|
||||
|
||||
manga.description = listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString())
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
}
|
||||
|
||||
fun PervEdenGalleryMetadata.copyTo(manga: SManga) {
|
||||
url?.let { manga.url = it }
|
||||
thumbnailUrl?.let { manga.thumbnail_url = it }
|
||||
|
||||
val titleDesc = StringBuilder()
|
||||
title?.let {
|
||||
manga.title = it
|
||||
titleDesc += "Title: $it\n"
|
||||
}
|
||||
if(altTitles.isNotEmpty())
|
||||
titleDesc += "Alternate Titles: \n" + altTitles.map {
|
||||
"▪ $it"
|
||||
}.joinToString(separator = "\n", postfix = "\n")
|
||||
|
||||
val detailsDesc = StringBuilder()
|
||||
artist?.let {
|
||||
manga.artist = it
|
||||
detailsDesc += "Artist: $it\n"
|
||||
}
|
||||
|
||||
type?.let {
|
||||
manga.genre = it
|
||||
detailsDesc += "Type: $it\n"
|
||||
}
|
||||
|
||||
status?.let {
|
||||
manga.status = when(it) {
|
||||
"Ongoing" -> SManga.ONGOING
|
||||
"Completed", "Suspended" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
detailsDesc += "Status: $it\n"
|
||||
}
|
||||
|
||||
rating?.let {
|
||||
detailsDesc += "Rating: %.2\n".format(it)
|
||||
}
|
||||
|
||||
val tagsDesc = buildTagsDescription(this)
|
||||
|
||||
manga.description = listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString())
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
}
|
||||
|
||||
fun NHentaiMetadata.copyTo(manga: SManga) {
|
||||
url?.let { manga.url = it }
|
||||
|
||||
//TODO next update allow this to be changed to use HD covers
|
||||
if(mediaId != null)
|
||||
NHentaiMetadata.typeToExtension(thumbnailImageType)?.let {
|
||||
manga.thumbnail_url = "https://t.nhentai.net/galleries/$mediaId/thumb.$it"
|
||||
}
|
||||
|
||||
manga.title = englishTitle ?: japaneseTitle ?: shortTitle!!
|
||||
|
||||
//Set artist (if we can find one)
|
||||
tags[NHENTAI_ARTIST_NAMESPACE]?.let {
|
||||
if(it.isNotEmpty()) manga.artist = it.joinToString(transform = Tag::name)
|
||||
}
|
||||
|
||||
tags[NHENTAI_CATEGORIES_NAMESPACE]?.let {
|
||||
if(it.isNotEmpty()) manga.genre = it.joinToString(transform = Tag::name)
|
||||
}
|
||||
|
||||
//Try to automatically identify if it is ongoing, we try not to be too lenient here to avoid making mistakes
|
||||
//We default to completed
|
||||
manga.status = SManga.COMPLETED
|
||||
englishTitle?.let { t ->
|
||||
ONGOING_SUFFIX.find {
|
||||
t.endsWith(it, ignoreCase = true)
|
||||
}?.let {
|
||||
manga.status = SManga.ONGOING
|
||||
}
|
||||
}
|
||||
|
||||
val titleDesc = StringBuilder()
|
||||
englishTitle?.let { titleDesc += "English Title: $it\n" }
|
||||
japaneseTitle?.let { titleDesc += "Japanese Title: $it\n" }
|
||||
shortTitle?.let { titleDesc += "Short Title: $it\n" }
|
||||
|
||||
val detailsDesc = StringBuilder()
|
||||
uploadDate?.let { detailsDesc += "Upload Date: ${EX_DATE_FORMAT.format(Date(it))}\n" }
|
||||
pageImageTypes.size.let { detailsDesc += "Length: $it pages\n" }
|
||||
favoritesCount?.let { detailsDesc += "Favorited: $it times\n" }
|
||||
scanlator?.nullIfBlank()?.let { detailsDesc += "Scanlator: $it\n" }
|
||||
|
||||
val tagsDesc = buildTagsDescription(this)
|
||||
|
||||
manga.description = listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString())
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
}
|
||||
|
||||
fun SearchableGalleryMetadata.genericCopyTo(manga: SManga): Boolean {
|
||||
when(this) {
|
||||
is ExGalleryMetadata -> this.copyTo(manga)
|
||||
is PervEdenGalleryMetadata -> this.copyTo(manga)
|
||||
is NHentaiMetadata -> this.copyTo(manga)
|
||||
else -> return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun buildTagsDescription(metadata: SearchableGalleryMetadata)
|
||||
= StringBuilder("Tags:\n").apply {
|
||||
//BiConsumer only available in Java 8, don't bother calling forEach directly on 'tags'
|
||||
metadata.tags.entries.forEach { namespace, tags ->
|
||||
if (tags.isNotEmpty()) {
|
||||
val joinedTags = tags.joinToString(separator = " ", transform = { "<${it.name}>" })
|
||||
this += "▪ $namespace: $joinedTags\n"
|
||||
}
|
||||
}
|
||||
}
|
51
app/src/main/java/exh/metadata/models/ExGalleryMetadata.kt
Executable file
51
app/src/main/java/exh/metadata/models/ExGalleryMetadata.kt
Executable file
@ -0,0 +1,51 @@
|
||||
package exh.metadata.models
|
||||
|
||||
import android.net.Uri
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Gallery metadata storage model
|
||||
*/
|
||||
|
||||
class ExGalleryMetadata : SearchableGalleryMetadata() {
|
||||
var url: String? = null
|
||||
|
||||
var exh: Boolean? = null
|
||||
|
||||
var thumbnailUrl: String? = null
|
||||
|
||||
var title: String? = null
|
||||
var altTitle: String? = null
|
||||
|
||||
var genre: String? = null
|
||||
|
||||
var datePosted: Long? = null
|
||||
var parent: String? = null
|
||||
var visible: String? = null //Not a boolean
|
||||
var language: String? = null
|
||||
var translated: Boolean? = null
|
||||
var size: Long? = null
|
||||
var length: Int? = null
|
||||
var favorites: Int? = null
|
||||
var ratingCount: Int? = null
|
||||
var averageRating: Double? = null
|
||||
|
||||
override fun getTitles() = listOf(title, altTitle).filterNotNull()
|
||||
|
||||
private fun splitGalleryUrl()
|
||||
= url?.let {
|
||||
Uri.parse(it).pathSegments.filterNot(String::isNullOrBlank)
|
||||
}
|
||||
|
||||
fun galleryId() = splitGalleryUrl()?.let { it[it.size - 2] }
|
||||
|
||||
fun galleryToken() =
|
||||
splitGalleryUrl()?.last()
|
||||
|
||||
override fun galleryUniqueIdentifier() = exh?.let { exh ->
|
||||
url?.let {
|
||||
//Fuck, this should be EXH and EH but it's too late to change it now...
|
||||
"${if(exh) "EXH" else "EX"}-${galleryId()}-${galleryToken()}"
|
||||
}
|
||||
}
|
||||
}
|
48
app/src/main/java/exh/metadata/models/NHentaiMetadata.kt
Executable file
48
app/src/main/java/exh/metadata/models/NHentaiMetadata.kt
Executable file
@ -0,0 +1,48 @@
|
||||
package exh.metadata.models
|
||||
|
||||
/**
|
||||
* NHentai metadata
|
||||
*/
|
||||
|
||||
class NHentaiMetadata : SearchableGalleryMetadata() {
|
||||
|
||||
var id: Long? = null
|
||||
|
||||
var url get() = id?.let { "$BASE_URL/g/$it" }
|
||||
set(a) {
|
||||
a?.let {
|
||||
id = a.split("/").last().toLong()
|
||||
}
|
||||
}
|
||||
|
||||
var uploadDate: Long? = null
|
||||
|
||||
var favoritesCount: Long? = null
|
||||
|
||||
var mediaId: String? = null
|
||||
|
||||
var japaneseTitle: String? = null
|
||||
var englishTitle: String? = null
|
||||
var shortTitle: String? = null
|
||||
|
||||
var coverImageType: String? = null
|
||||
var pageImageTypes: MutableList<String> = mutableListOf()
|
||||
var thumbnailImageType: String? = null
|
||||
|
||||
var scanlator: String? = null
|
||||
|
||||
override fun galleryUniqueIdentifier(): String? = "NHENTAI-$id"
|
||||
|
||||
override fun getTitles() = listOf(japaneseTitle, englishTitle, shortTitle).filterNotNull()
|
||||
|
||||
companion object {
|
||||
val BASE_URL = "https://nhentai.net"
|
||||
|
||||
fun typeToExtension(t: String?) =
|
||||
when(t) {
|
||||
"p" -> "png"
|
||||
"j" -> "jpg"
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
32
app/src/main/java/exh/metadata/models/PervEdenGalleryMetadata.kt
Executable file
32
app/src/main/java/exh/metadata/models/PervEdenGalleryMetadata.kt
Executable file
@ -0,0 +1,32 @@
|
||||
package exh.metadata.models
|
||||
|
||||
import android.net.Uri
|
||||
|
||||
class PervEdenGalleryMetadata : SearchableGalleryMetadata() {
|
||||
var url: String? = null
|
||||
var thumbnailUrl: String? = null
|
||||
|
||||
var title: String? = null
|
||||
var altTitles: MutableList<String> = mutableListOf()
|
||||
|
||||
var artist: String? = null
|
||||
|
||||
var type: String? = null
|
||||
|
||||
var rating: Float? = null
|
||||
|
||||
var status: String? = null
|
||||
|
||||
var lang: String? = null
|
||||
|
||||
override fun getTitles() = listOf(title).plus(altTitles).filterNotNull()
|
||||
|
||||
private fun splitGalleryUrl()
|
||||
= url?.let {
|
||||
Uri.parse(it).pathSegments.filterNot(String::isNullOrBlank)
|
||||
}
|
||||
|
||||
override fun galleryUniqueIdentifier() = splitGalleryUrl()?.let {
|
||||
"PERVEDEN-${lang?.toUpperCase()}-${it.last()}"
|
||||
}
|
||||
}
|
18
app/src/main/java/exh/metadata/models/SearchableGalleryMetadata.kt
Executable file
18
app/src/main/java/exh/metadata/models/SearchableGalleryMetadata.kt
Executable file
@ -0,0 +1,18 @@
|
||||
package exh.metadata.models
|
||||
|
||||
import java.util.ArrayList
|
||||
import java.util.HashMap
|
||||
|
||||
/**
|
||||
* A gallery that can be searched using the EH search engine
|
||||
*/
|
||||
abstract class SearchableGalleryMetadata {
|
||||
var uploader: String? = null
|
||||
|
||||
//Being specific about which classes are used in generics to make deserialization easier
|
||||
val tags: HashMap<String, ArrayList<Tag>> = HashMap()
|
||||
|
||||
abstract fun galleryUniqueIdentifier(): String?
|
||||
|
||||
abstract fun getTitles(): List<String>
|
||||
}
|
7
app/src/main/java/exh/metadata/models/Tag.kt
Executable file
7
app/src/main/java/exh/metadata/models/Tag.kt
Executable file
@ -0,0 +1,7 @@
|
||||
package exh.metadata.models
|
||||
|
||||
/**
|
||||
* Simple tag model
|
||||
*/
|
||||
|
||||
data class Tag(var name: String, var light: Boolean)
|
3
app/src/main/java/exh/search/MultiWildcard.kt
Executable file
3
app/src/main/java/exh/search/MultiWildcard.kt
Executable file
@ -0,0 +1,3 @@
|
||||
package exh.search
|
||||
|
||||
class MultiWildcard : TextComponent()
|
4
app/src/main/java/exh/search/Namespace.kt
Executable file
4
app/src/main/java/exh/search/Namespace.kt
Executable file
@ -0,0 +1,4 @@
|
||||
package exh.search
|
||||
|
||||
class Namespace(var namespace: String,
|
||||
var tag: Text? = null) : QueryComponent()
|
6
app/src/main/java/exh/search/QueryComponent.kt
Executable file
6
app/src/main/java/exh/search/QueryComponent.kt
Executable file
@ -0,0 +1,6 @@
|
||||
package exh.search
|
||||
|
||||
open class QueryComponent {
|
||||
var excluded = false
|
||||
var exact = false
|
||||
}
|
150
app/src/main/java/exh/search/SearchEngine.kt
Executable file
150
app/src/main/java/exh/search/SearchEngine.kt
Executable file
@ -0,0 +1,150 @@
|
||||
package exh.search
|
||||
|
||||
import exh.metadata.models.SearchableGalleryMetadata
|
||||
import exh.metadata.models.Tag
|
||||
|
||||
class SearchEngine {
|
||||
|
||||
private val queryCache = mutableMapOf<String, List<QueryComponent>>()
|
||||
|
||||
fun matches(metadata: SearchableGalleryMetadata, query: List<QueryComponent>): Boolean {
|
||||
|
||||
fun matchTagList(tags: Sequence<Tag>,
|
||||
component: Text): Boolean {
|
||||
//Match tags
|
||||
val tagMatcher = if(!component.exact)
|
||||
component.asLenientRegex()
|
||||
else
|
||||
component.asRegex()
|
||||
//Match beginning of tag
|
||||
if (tags.find {
|
||||
tagMatcher.testExact(it.name)
|
||||
} != null) {
|
||||
if(component.excluded) return false
|
||||
} else {
|
||||
//No tag matched for this component
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
val cachedTitles = metadata.getTitles().map(String::toLowerCase)
|
||||
|
||||
for(component in query) {
|
||||
if(component is Text) {
|
||||
//Match title
|
||||
if (cachedTitles.find { component.asRegex().test(it) } != null) {
|
||||
continue
|
||||
}
|
||||
//Match tags
|
||||
if(!matchTagList(metadata.tags.entries.asSequence().flatMap { it.value.asSequence() },
|
||||
component)) return false
|
||||
} else if(component is Namespace) {
|
||||
if(component.namespace == "uploader") {
|
||||
//Match uploader
|
||||
if(!component.tag?.rawTextOnly().equals(metadata.uploader,
|
||||
ignoreCase = true)) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
if(component.tag!!.components.size > 0) {
|
||||
//Match namespace
|
||||
val ns = metadata.tags.entries.asSequence().filter {
|
||||
it.key == component.namespace
|
||||
}.flatMap { it.value.asSequence() }
|
||||
//Match tags
|
||||
if (!matchTagList(ns, component.tag!!))
|
||||
return false
|
||||
} else {
|
||||
//Perform namespace search
|
||||
val hasNs = metadata.tags.entries.find {
|
||||
it.key == component.namespace
|
||||
} != null
|
||||
|
||||
if(hasNs && component.excluded)
|
||||
return false
|
||||
else if(!hasNs && !component.excluded)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun parseQuery(query: String) = queryCache.getOrPut(query, {
|
||||
val res = mutableListOf<QueryComponent>()
|
||||
|
||||
var inQuotes = false
|
||||
val queuedRawText = StringBuilder()
|
||||
val queuedText = mutableListOf<TextComponent>()
|
||||
var namespace: Namespace? = null
|
||||
|
||||
var nextIsExcluded = false
|
||||
var nextIsExact = false
|
||||
|
||||
fun flushText() {
|
||||
if(queuedRawText.isNotEmpty()) {
|
||||
queuedText += StringTextComponent(queuedRawText.toString())
|
||||
queuedRawText.setLength(0)
|
||||
}
|
||||
}
|
||||
|
||||
fun flushToText() = Text().apply {
|
||||
components += queuedText
|
||||
queuedText.clear()
|
||||
}
|
||||
|
||||
fun flushAll() {
|
||||
flushText()
|
||||
if (queuedText.isNotEmpty() || namespace != null) {
|
||||
val component = namespace?.apply {
|
||||
tag = flushToText()
|
||||
namespace = null
|
||||
} ?: flushToText()
|
||||
component.excluded = nextIsExcluded
|
||||
component.exact = nextIsExact
|
||||
res += component
|
||||
}
|
||||
}
|
||||
|
||||
for(char in query.toLowerCase()) {
|
||||
if(char == '"') {
|
||||
inQuotes = !inQuotes
|
||||
} else if(char == '?' || char == '_') {
|
||||
flushText()
|
||||
queuedText.add(SingleWildcard())
|
||||
} else if(char == '*' || char == '%') {
|
||||
flushText()
|
||||
queuedText.add(MultiWildcard())
|
||||
} else if(char == '-') {
|
||||
nextIsExcluded = true
|
||||
} else if(char == '$') {
|
||||
nextIsExact = true
|
||||
} else if(char == ':') {
|
||||
flushText()
|
||||
var flushed = flushToText().rawTextOnly()
|
||||
//Map tag aliases
|
||||
flushed = when(flushed) {
|
||||
"a" -> "artist"
|
||||
"c", "char" -> "character"
|
||||
"f" -> "female"
|
||||
"g", "creator", "circle" -> "group"
|
||||
"l", "lang" -> "language"
|
||||
"m" -> "male"
|
||||
"p", "series" -> "parody"
|
||||
"r" -> "reclass"
|
||||
else -> flushed
|
||||
}
|
||||
namespace = Namespace(flushed, null)
|
||||
} else if(char == ' ' && !inQuotes) {
|
||||
flushAll()
|
||||
} else {
|
||||
queuedRawText.append(char)
|
||||
}
|
||||
}
|
||||
flushAll()
|
||||
|
||||
res
|
||||
})
|
||||
}
|
3
app/src/main/java/exh/search/SingleWildcard.kt
Executable file
3
app/src/main/java/exh/search/SingleWildcard.kt
Executable file
@ -0,0 +1,3 @@
|
||||
package exh.search
|
||||
|
||||
class SingleWildcard : TextComponent()
|
3
app/src/main/java/exh/search/StringTextComponent.kt
Executable file
3
app/src/main/java/exh/search/StringTextComponent.kt
Executable file
@ -0,0 +1,3 @@
|
||||
package exh.search
|
||||
|
||||
class StringTextComponent(val value: String) : TextComponent()
|
49
app/src/main/java/exh/search/Text.kt
Executable file
49
app/src/main/java/exh/search/Text.kt
Executable file
@ -0,0 +1,49 @@
|
||||
package exh.search
|
||||
|
||||
import exh.anyChar
|
||||
import ru.lanwen.verbalregex.VerbalExpression
|
||||
|
||||
class Text: QueryComponent() {
|
||||
val components = mutableListOf<TextComponent>()
|
||||
|
||||
private var regex: VerbalExpression? = null
|
||||
private var lenientRegex: VerbalExpression? = null
|
||||
private var rawText: String? = null
|
||||
|
||||
fun asRegex(): VerbalExpression {
|
||||
if(regex == null) {
|
||||
regex = baseBuilder().build()
|
||||
}
|
||||
return regex!!
|
||||
}
|
||||
|
||||
fun asLenientRegex(): VerbalExpression {
|
||||
if(lenientRegex == null) {
|
||||
lenientRegex = baseBuilder().anything().build()
|
||||
}
|
||||
return lenientRegex!!
|
||||
}
|
||||
|
||||
fun baseBuilder(): VerbalExpression.Builder {
|
||||
val builder = VerbalExpression.regex()
|
||||
for(component in components) {
|
||||
when(component) {
|
||||
is StringTextComponent -> builder.then(component.value)
|
||||
is SingleWildcard -> builder.anyChar()
|
||||
is MultiWildcard -> builder.anything()
|
||||
}
|
||||
}
|
||||
return builder
|
||||
}
|
||||
|
||||
fun rawTextOnly() = if(rawText != null)
|
||||
rawText!!
|
||||
else {
|
||||
rawText = components
|
||||
.filter { it is StringTextComponent }
|
||||
.joinToString(separator = "", transform = {
|
||||
(it as StringTextComponent).value
|
||||
})
|
||||
rawText!!
|
||||
}
|
||||
}
|
3
app/src/main/java/exh/search/TextComponent.kt
Executable file
3
app/src/main/java/exh/search/TextComponent.kt
Executable file
@ -0,0 +1,3 @@
|
||||
package exh.search
|
||||
|
||||
open class TextComponent
|
131
app/src/main/java/exh/ui/batchadd/BatchAddFragment.kt
Executable file
131
app/src/main/java/exh/ui/batchadd/BatchAddFragment.kt
Executable file
@ -0,0 +1,131 @@
|
||||
package exh.ui.batchadd
|
||||
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment
|
||||
import exh.GalleryAdder
|
||||
import exh.metadata.nullIfBlank
|
||||
import kotlinx.android.synthetic.main.eh_fragment_batch_add.*
|
||||
import timber.log.Timber
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
/**
|
||||
* LoginActivity
|
||||
*/
|
||||
|
||||
class BatchAddFragment : BaseFragment() {
|
||||
|
||||
private val galleryAdder by lazy { GalleryAdder() }
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?)
|
||||
= inflater.inflate(R.layout.eh_fragment_batch_add, container, false)!!
|
||||
|
||||
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
|
||||
setToolbarTitle("Batch add")
|
||||
|
||||
setup()
|
||||
}
|
||||
|
||||
fun setup() {
|
||||
btn_add_galleries.setOnClickListener {
|
||||
val galleries = galleries_box.text.toString()
|
||||
//Check text box has content
|
||||
if(galleries.isNullOrBlank()) {
|
||||
noGalleriesSpecified()
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
//Too lazy to actually deal with orientation changes
|
||||
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
|
||||
|
||||
val splitGalleries = galleries.split("\n").map {
|
||||
it.trim().nullIfBlank()
|
||||
}.filterNotNull()
|
||||
|
||||
val dialog = MaterialDialog.Builder(context)
|
||||
.title("Adding galleries...")
|
||||
.progress(false, splitGalleries.size, true)
|
||||
.cancelable(false)
|
||||
.canceledOnTouchOutside(false)
|
||||
.show()
|
||||
|
||||
val succeeded = mutableListOf<String>()
|
||||
val failed = mutableListOf<String>()
|
||||
|
||||
thread {
|
||||
splitGalleries.forEachIndexed { i, s ->
|
||||
activity.runOnUiThread {
|
||||
dialog.setContent("Processing: $s")
|
||||
}
|
||||
if(addGallery(s)) {
|
||||
succeeded.add(s)
|
||||
} else {
|
||||
failed.add(s)
|
||||
}
|
||||
activity.runOnUiThread {
|
||||
dialog.setProgress(i + 1)
|
||||
}
|
||||
}
|
||||
|
||||
//Show report
|
||||
val succeededCount = succeeded.size
|
||||
val failedCount = failed.size
|
||||
|
||||
if(succeeded.isEmpty()) succeeded += "None"
|
||||
if(failed.isEmpty()) failed += "None"
|
||||
val succeededReport = succeeded.joinToString(separator = "\n", prefix = "Added:\n")
|
||||
val failedReport = failed.joinToString(separator = "\n", prefix = "Failed:\n")
|
||||
|
||||
val summary = "Summary:\nAdded: $succeededCount gallerie(s)\nFailed: $failedCount gallerie(s)"
|
||||
|
||||
val report = listOf(succeededReport, failedReport, summary).joinToString(separator = "\n\n")
|
||||
|
||||
activity.runOnUiThread {
|
||||
//Enable orientation changes again
|
||||
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR
|
||||
|
||||
dialog.dismiss()
|
||||
|
||||
MaterialDialog.Builder(context)
|
||||
.title("Batch add report")
|
||||
.content(report)
|
||||
.positiveText("Ok")
|
||||
.cancelable(true)
|
||||
.canceledOnTouchOutside(true)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fun addGallery(url: String): Boolean {
|
||||
try {
|
||||
galleryAdder.addGallery(url, true)
|
||||
} catch(t: Throwable) {
|
||||
Timber.e(t, "Could not add gallery!")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun noGalleriesSpecified() {
|
||||
MaterialDialog.Builder(context)
|
||||
.title("No galleries to add!")
|
||||
.content("You must specify at least one gallery to add!")
|
||||
.positiveText("Ok")
|
||||
.onPositive { materialDialog, _ -> materialDialog.dismiss() }
|
||||
.cancelable(true)
|
||||
.canceledOnTouchOutside(true)
|
||||
.show()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance() = BatchAddFragment()
|
||||
}
|
||||
}
|
82
app/src/main/java/exh/ui/intercept/InterceptActivity.kt
Executable file
82
app/src/main/java/exh/ui/intercept/InterceptActivity.kt
Executable file
@ -0,0 +1,82 @@
|
||||
package exh.ui.intercept
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaActivity
|
||||
import exh.GalleryAdder
|
||||
import kotlinx.android.synthetic.main.toolbar.*
|
||||
import timber.log.Timber
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
class InterceptActivity : BaseActivity() {
|
||||
|
||||
private val galleryAdder = GalleryAdder()
|
||||
|
||||
var finished = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setAppTheme()
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.eh_activity_intercept)
|
||||
|
||||
setupToolbar(toolbar, backNavigation = false)
|
||||
|
||||
if(savedInstanceState == null)
|
||||
thread { setup() }
|
||||
}
|
||||
|
||||
fun setup() {
|
||||
try {
|
||||
processLink()
|
||||
} catch(t: Throwable) {
|
||||
Timber.e(t, "Could not intercept link!")
|
||||
if(!finished)
|
||||
runOnUiThread {
|
||||
MaterialDialog.Builder(this)
|
||||
.title("Error")
|
||||
.content("Could not load this gallery!")
|
||||
.cancelable(true)
|
||||
.canceledOnTouchOutside(true)
|
||||
.cancelListener { onBackPressed() }
|
||||
.positiveText("Ok")
|
||||
.onPositive { _, _ -> onBackPressed() }
|
||||
.dismissListener { onBackPressed() }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun processLink() {
|
||||
if(Intent.ACTION_VIEW == intent.action) {
|
||||
val manga = galleryAdder.addGallery(intent.dataString)
|
||||
|
||||
if(!finished)
|
||||
startActivity(MangaActivity.newIntent(this, manga, true))
|
||||
onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> onBackPressed()
|
||||
else -> return super.onOptionsItemSelected(item)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if(!finished)
|
||||
runOnUiThread {
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
finished = true
|
||||
}
|
||||
}
|
60
app/src/main/java/exh/ui/lock/LockActivity.kt
Executable file
60
app/src/main/java/exh/ui/lock/LockActivity.kt
Executable file
@ -0,0 +1,60 @@
|
||||
package exh.ui.lock
|
||||
|
||||
import android.os.Bundle
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.andrognito.pinlockview.PinLockListener
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
||||
import kotlinx.android.synthetic.main.activity_lock.*
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class LockActivity : BaseActivity() {
|
||||
|
||||
val prefs: PreferencesHelper by injectLazy()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
disableLock = true
|
||||
|
||||
setTheme(R.style.Theme_Tachiyomi_Dark)
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if(!lockEnabled(prefs)) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
setContentView(R.layout.activity_lock)
|
||||
|
||||
pin_lock_view.attachIndicatorDots(indicator_dots)
|
||||
|
||||
pin_lock_view.pinLength = prefs.lockLength().getOrDefault()
|
||||
pin_lock_view.setPinLockListener(object : PinLockListener {
|
||||
override fun onEmpty() {}
|
||||
|
||||
override fun onComplete(pin: String) {
|
||||
if(sha512(pin, prefs.lockSalt().get()!!) == prefs.lockHash().get()) {
|
||||
//Yay!
|
||||
finish()
|
||||
} else {
|
||||
MaterialDialog.Builder(this@LockActivity)
|
||||
.title("PIN code incorrect")
|
||||
.content("The PIN code you entered is incorrect. Please try again.")
|
||||
.cancelable(true)
|
||||
.canceledOnTouchOutside(true)
|
||||
.positiveText("Ok")
|
||||
.autoDismiss(true)
|
||||
.show()
|
||||
pin_lock_view.resetPinLockView()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPinChange(pinLength: Int, intermediatePin: String?) {}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
moveTaskToBack(true)
|
||||
}
|
||||
}
|
85
app/src/main/java/exh/ui/lock/LockPreference.kt
Executable file
85
app/src/main/java/exh/ui/lock/LockPreference.kt
Executable file
@ -0,0 +1,85 @@
|
||||
package exh.ui.lock
|
||||
|
||||
import android.content.Context
|
||||
import android.support.v7.preference.Preference
|
||||
import android.text.InputType
|
||||
import android.util.AttributeSet
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.math.BigInteger
|
||||
import java.security.SecureRandom
|
||||
|
||||
class LockPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
||||
Preference(context, attrs) {
|
||||
|
||||
val secureRandom by lazy { SecureRandom() }
|
||||
|
||||
val prefs: PreferencesHelper by injectLazy()
|
||||
|
||||
override fun onAttached() {
|
||||
super.onAttached()
|
||||
updateSummary()
|
||||
}
|
||||
|
||||
fun updateSummary() {
|
||||
if(lockEnabled(prefs)) {
|
||||
summary = "Application is locked"
|
||||
} else {
|
||||
summary = "Application is not locked, tap to lock"
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
super.onClick()
|
||||
if(!notifyLockSecurity(context)) {
|
||||
MaterialDialog.Builder(context)
|
||||
.title("Lock application")
|
||||
.content("Enter a pin to lock the application. Enter nothing to disable the pin lock.")
|
||||
.inputRangeRes(0, 10, R.color.material_red_500)
|
||||
.inputType(InputType.TYPE_CLASS_NUMBER)
|
||||
.input("", "", { _, c ->
|
||||
val progressDialog = MaterialDialog.Builder(context)
|
||||
.title("Saving password")
|
||||
.progress(true, 0)
|
||||
.cancelable(false)
|
||||
.show()
|
||||
Observable.fromCallable {
|
||||
savePassword(c.toString())
|
||||
}.subscribeOn(Schedulers.computation())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
progressDialog.dismiss()
|
||||
updateSummary()
|
||||
}
|
||||
})
|
||||
.negativeText("Cancel")
|
||||
.autoDismiss(true)
|
||||
.cancelable(true)
|
||||
.canceledOnTouchOutside(true)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
fun savePassword(password: String) {
|
||||
val salt: String?
|
||||
val hash: String?
|
||||
val length: Int
|
||||
if(password.isEmpty()) {
|
||||
salt = null
|
||||
hash = null
|
||||
length = -1
|
||||
} else {
|
||||
salt = BigInteger(130, secureRandom).toString(32)
|
||||
hash = sha512(password, salt)
|
||||
length = password.length
|
||||
}
|
||||
prefs.lockSalt().set(salt)
|
||||
prefs.lockHash().set(hash)
|
||||
prefs.lockLength().set(length)
|
||||
}
|
||||
}
|
91
app/src/main/java/exh/ui/lock/LockUtils.kt
Executable file
91
app/src/main/java/exh/ui/lock/LockUtils.kt
Executable file
@ -0,0 +1,91 @@
|
||||
package exh.ui.lock
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.app.Activity
|
||||
import android.app.AppOpsManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.security.MessageDigest
|
||||
import kotlin.experimental.and
|
||||
|
||||
|
||||
/**
|
||||
* Password hashing utils
|
||||
*/
|
||||
|
||||
/**
|
||||
* Yes, I know SHA512 is fast, but bcrypt on mobile devices is too slow apparently
|
||||
*/
|
||||
fun sha512(passwordToHash: String, salt: String): String {
|
||||
val md = MessageDigest.getInstance("SHA-512")
|
||||
md.update(salt.toByteArray(charset("UTF-8")))
|
||||
val bytes = md.digest(passwordToHash.toByteArray(charset("UTF-8")))
|
||||
val sb = StringBuilder()
|
||||
for (i in bytes.indices) {
|
||||
sb.append(Integer.toString((bytes[i] and 0xff.toByte()) + 0x100, 16).substring(1))
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if lock is enabled
|
||||
*/
|
||||
fun lockEnabled(prefs: PreferencesHelper = Injekt.get())
|
||||
= prefs.lockHash().get() != null
|
||||
&& prefs.lockSalt().get() != null
|
||||
&& prefs.lockLength().getOrDefault() != -1
|
||||
|
||||
/**
|
||||
* Lock the screen
|
||||
*/
|
||||
fun showLockActivity(activity: Activity) {
|
||||
activity.startActivity(Intent(activity, LockActivity::class.java))
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the lock will function properly
|
||||
*
|
||||
* @return true if action is required, false if lock is working properly
|
||||
*/
|
||||
fun notifyLockSecurity(context: Context): Boolean {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !hasAccessToUsageStats(context)) {
|
||||
MaterialDialog.Builder(context)
|
||||
.title("Permission required")
|
||||
.content("${context.getString(R.string.app_name)} requires the usage stats permission to detect when you leave the app. " +
|
||||
"This is required for the application lock to function properly. " +
|
||||
"Press OK to grant this permission now.")
|
||||
.negativeText("Cancel")
|
||||
.positiveText("Ok")
|
||||
.onPositive { _, _ ->
|
||||
context.startActivity(Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS))
|
||||
}
|
||||
.autoDismiss(true)
|
||||
.cancelable(false)
|
||||
.show()
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
fun hasAccessToUsageStats(context: Context): Boolean {
|
||||
try {
|
||||
val packageManager = context.packageManager
|
||||
val applicationInfo = packageManager.getApplicationInfo(context.packageName, 0)
|
||||
val appOpsManager = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
|
||||
val mode = appOpsManager.checkOpNoThrow(AppOpsManager.OPSTR_GET_USAGE_STATS, applicationInfo.uid, applicationInfo.packageName)
|
||||
return (mode == AppOpsManager.MODE_ALLOWED)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
return false
|
||||
}
|
||||
}
|
218
app/src/main/java/exh/ui/login/LoginActivity.kt
Executable file
218
app/src/main/java/exh/ui/login/LoginActivity.kt
Executable file
@ -0,0 +1,218 @@
|
||||
package exh.ui.login
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.online.all.EHentai
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
||||
import exh.EXH_SOURCE_ID
|
||||
import kotlinx.android.synthetic.main.eh_activity_login.*
|
||||
import kotlinx.android.synthetic.main.toolbar.*
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.net.HttpCookie
|
||||
|
||||
/**
|
||||
* LoginActivity
|
||||
*/
|
||||
|
||||
class LoginActivity : BaseActivity() {
|
||||
|
||||
val preferenceManager: PreferencesHelper by injectLazy()
|
||||
|
||||
val sourceManager: SourceManager by injectLazy()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setAppTheme()
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.eh_activity_login)
|
||||
|
||||
setup()
|
||||
|
||||
setupToolbar(toolbar, backNavigation = false)
|
||||
}
|
||||
|
||||
fun setup() {
|
||||
btn_cancel.setOnClickListener { onBackPressed() }
|
||||
btn_recheck.setOnClickListener { webview.loadUrl("http://exhentai.org/") }
|
||||
|
||||
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
CookieManager.getInstance().removeAllCookies {
|
||||
runOnUiThread {
|
||||
startWebview()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
CookieManager.getInstance().removeAllCookie()
|
||||
startWebview()
|
||||
}
|
||||
}
|
||||
|
||||
fun startWebview() {
|
||||
webview.settings.javaScriptEnabled = true
|
||||
webview.settings.domStorageEnabled = true
|
||||
|
||||
webview.loadUrl("https://forums.e-hentai.org/index.php?act=Login")
|
||||
|
||||
webview.setWebViewClient(object : WebViewClient() {
|
||||
override fun onPageFinished(view: WebView, url: String) {
|
||||
super.onPageFinished(view, url)
|
||||
Timber.d(url)
|
||||
val parsedUrl = Uri.parse(url)
|
||||
if(parsedUrl.host.equals("forums.e-hentai.org", ignoreCase = true)) {
|
||||
//Hide distracting content
|
||||
view.loadUrl(HIDE_JS)
|
||||
|
||||
//Check login result
|
||||
if(parsedUrl.getQueryParameter("code")?.toInt() != 0) {
|
||||
if(checkLoginCookies(url)) view.loadUrl("http://exhentai.org/")
|
||||
}
|
||||
} else if(parsedUrl.host.equals("exhentai.org", ignoreCase = true)) {
|
||||
//At ExHentai, check that everything worked out...
|
||||
if(applyExHentaiCookies(url)) {
|
||||
preferenceManager.enableExhentai().set(true)
|
||||
finishLogin()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun finishLogin() {
|
||||
val progressDialog = MaterialDialog.Builder(this)
|
||||
.title("Finalizing login")
|
||||
.progress(true, 0)
|
||||
.content("Please wait...")
|
||||
.cancelable(false)
|
||||
.show()
|
||||
|
||||
val eh = sourceManager
|
||||
.getOnlineSources()
|
||||
.find { it.id == EXH_SOURCE_ID } as EHentai
|
||||
Observable.fromCallable {
|
||||
//I honestly have no idea why we need to call this twice, but it works, so whatever
|
||||
try {
|
||||
eh.fetchFavorites()
|
||||
} catch(ignored: Exception) {}
|
||||
try {
|
||||
eh.fetchFavorites()
|
||||
} catch(ignored: Exception) {}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
progressDialog.dismiss()
|
||||
onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we are logged in
|
||||
*/
|
||||
fun checkLoginCookies(url: String): Boolean {
|
||||
getCookies(url)?.let { parsed ->
|
||||
return parsed.filter {
|
||||
(it.name.equals(MEMBER_ID_COOKIE, ignoreCase = true)
|
||||
|| it.name.equals(PASS_HASH_COOKIE, ignoreCase = true))
|
||||
&& it.value.isNotBlank()
|
||||
}.count() >= 2
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse cookies at ExHentai
|
||||
*/
|
||||
fun applyExHentaiCookies(url: String): Boolean {
|
||||
getCookies(url)?.let { parsed ->
|
||||
|
||||
var memberId: String? = null
|
||||
var passHash: String? = null
|
||||
var igneous: String? = null
|
||||
|
||||
parsed.forEach {
|
||||
when (it.name.toLowerCase()) {
|
||||
MEMBER_ID_COOKIE -> memberId = it.value
|
||||
PASS_HASH_COOKIE -> passHash = it.value
|
||||
IGNEOUS_COOKIE -> igneous = it.value
|
||||
}
|
||||
}
|
||||
|
||||
//Missing a cookie
|
||||
if (memberId == null || passHash == null || igneous == null) return false
|
||||
|
||||
//Update prefs
|
||||
preferenceManager.memberIdVal().set(memberId)
|
||||
preferenceManager.passHashVal().set(passHash)
|
||||
preferenceManager.igneousVal().set(igneous)
|
||||
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun getCookies(url: String): List<HttpCookie>?
|
||||
= CookieManager.getInstance().getCookie(url)?.let {
|
||||
it.split("; ").flatMap {
|
||||
HttpCookie.parse(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> onBackPressed()
|
||||
else -> return super.onOptionsItemSelected(item)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MEMBER_ID_COOKIE = "ipb_member_id"
|
||||
const val PASS_HASH_COOKIE = "ipb_pass_hash"
|
||||
const val IGNEOUS_COOKIE = "igneous"
|
||||
|
||||
const val HIDE_JS = """
|
||||
javascript:(function () {
|
||||
document.getElementsByTagName('body')[0].style.visibility = 'hidden';
|
||||
document.getElementsByName('submit')[0].style.visibility = 'visible';
|
||||
document.querySelector('td[width="60%"][valign="top"]').style.visibility = 'visible';
|
||||
|
||||
function hide(e) {if(e !== null && e !== undefined) e.style.display = 'none';}
|
||||
|
||||
hide(document.querySelector(".errorwrap"));
|
||||
hide(document.querySelector('td[width="40%"][valign="top"]'));
|
||||
var child = document.querySelector(".page").querySelector('div');
|
||||
child.style.padding = null;
|
||||
var ft = child.querySelectorAll('table');
|
||||
var fd = child.parentNode.querySelectorAll('div > div');
|
||||
var fh = document.querySelector('#border').querySelectorAll('td > table');
|
||||
hide(ft[0]);
|
||||
hide(ft[1]);
|
||||
hide(fd[1]);
|
||||
hide(fd[2]);
|
||||
hide(child.querySelector('br'));
|
||||
var error = document.querySelector(".page > div > .borderwrap");
|
||||
if(error !== null) error.style.visibility = 'visible';
|
||||
hide(fh[0]);
|
||||
hide(fh[1]);
|
||||
hide(document.querySelector("#gfooter"));
|
||||
hide(document.querySelector(".copyright"));
|
||||
document.querySelectorAll("td").forEach(function(e) {
|
||||
e.style.color = "white";
|
||||
});
|
||||
var pc = document.querySelector(".postcolor");
|
||||
if(pc !== null) pc.style.color = "#26353F";
|
||||
})()
|
||||
"""
|
||||
}
|
||||
}
|
136
app/src/main/java/exh/ui/migration/MetadataFetchDialog.kt
Executable file
136
app/src/main/java/exh/ui/migration/MetadataFetchDialog.kt
Executable file
@ -0,0 +1,136 @@
|
||||
package exh.ui.migration
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.text.Html
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.online.all.EHentai
|
||||
import exh.isExSource
|
||||
import exh.isLewdSource
|
||||
import exh.metadata.MetadataHelper
|
||||
import exh.metadata.copyTo
|
||||
import exh.metadata.genericCopyTo
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
class MetadataFetchDialog {
|
||||
|
||||
val metadataHelper by lazy { MetadataHelper() }
|
||||
|
||||
val db: DatabaseHelper by injectLazy()
|
||||
|
||||
val sourceManager: SourceManager by injectLazy()
|
||||
|
||||
val preferenceHelper: PreferencesHelper by injectLazy()
|
||||
|
||||
fun show(context: Activity) {
|
||||
//Too lazy to actually deal with orientation changes
|
||||
context.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
|
||||
|
||||
val progressDialog = MaterialDialog.Builder(context)
|
||||
.title("Fetching library metadata")
|
||||
.content("Preparing library")
|
||||
.progress(false, 0, true)
|
||||
.cancelable(false)
|
||||
.canceledOnTouchOutside(false)
|
||||
.show()
|
||||
|
||||
thread {
|
||||
db.deleteMangasNotInLibrary().executeAsBlocking()
|
||||
|
||||
val libraryMangas = db.getLibraryMangas()
|
||||
.executeAsBlocking()
|
||||
.filter {
|
||||
isLewdSource(it.source)
|
||||
&& metadataHelper.fetchMetadata(it.url, it.source) == null
|
||||
}
|
||||
|
||||
context.runOnUiThread {
|
||||
progressDialog.maxProgress = libraryMangas.size
|
||||
}
|
||||
|
||||
//Actual metadata fetch code
|
||||
libraryMangas.forEachIndexed { i, manga ->
|
||||
context.runOnUiThread {
|
||||
progressDialog.setContent("Processing: ${manga.title}")
|
||||
progressDialog.setProgress(i + 1)
|
||||
}
|
||||
try {
|
||||
val source = sourceManager.get(manga.source)
|
||||
source?.let {
|
||||
manga.copyFrom(it.fetchMangaDetails(manga).toBlocking().first())
|
||||
metadataHelper.fetchMetadata(manga.url, manga.source)?.genericCopyTo(manga)
|
||||
}
|
||||
} catch(t: Throwable) {
|
||||
Timber.e(t, "Could not migrate manga!")
|
||||
}
|
||||
}
|
||||
|
||||
context.runOnUiThread {
|
||||
progressDialog.dismiss()
|
||||
|
||||
//Enable orientation changes again
|
||||
context.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
|
||||
|
||||
displayMigrationComplete(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun askMigration(activity: Activity) {
|
||||
var extra = ""
|
||||
db.getLibraryMangas().asRxSingle().subscribe {
|
||||
//Not logged in but have ExHentai galleries
|
||||
if(!preferenceHelper.enableExhentai().getOrDefault()) {
|
||||
it.find { isExSource(it.source) }?.let {
|
||||
extra = "<b><font color='red'>If you use ExHentai, please log in first before fetching your library metadata!</font></b><br><br>"
|
||||
}
|
||||
}
|
||||
activity.runOnUiThread {
|
||||
MaterialDialog.Builder(activity)
|
||||
.title("Fetch library metadata")
|
||||
.content(Html.fromHtml("You need to fetch your library's metadata before tag searching in the library will function.<br><br>" +
|
||||
"This process may take a long time depending on your library size and will also use up a significant amount of internet bandwidth.<br><br>" +
|
||||
extra +
|
||||
"This process can be done later if required."))
|
||||
.positiveText("Migrate")
|
||||
.negativeText("Later")
|
||||
.onPositive { _, _ -> show(activity) }
|
||||
.onNegative({ _, _ -> adviseMigrationLater(activity) })
|
||||
.cancelable(false)
|
||||
.canceledOnTouchOutside(false)
|
||||
.dismissListener {
|
||||
preferenceHelper.migrateLibraryAsked().set(true)
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun adviseMigrationLater(activity: Activity) {
|
||||
MaterialDialog.Builder(activity)
|
||||
.title("Metadata fetch canceled")
|
||||
.content("Library metadata fetch has been canceled.\n\n" +
|
||||
"You can run this operation later by going to: Settings > E-Hentai > Migrate library metadata")
|
||||
.positiveText("Ok")
|
||||
.cancelable(true)
|
||||
.canceledOnTouchOutside(true)
|
||||
.show()
|
||||
}
|
||||
|
||||
fun displayMigrationComplete(activity: Activity) {
|
||||
MaterialDialog.Builder(activity)
|
||||
.title("Migration complete")
|
||||
.content("${activity.getString(R.string.app_name)} is now ready for use!")
|
||||
.positiveText("Ok")
|
||||
.cancelable(true)
|
||||
.canceledOnTouchOutside(true)
|
||||
.show()
|
||||
}
|
||||
}
|
16
app/src/main/java/exh/ui/migration/MigrationStatus.kt
Executable file
16
app/src/main/java/exh/ui/migration/MigrationStatus.kt
Executable file
@ -0,0 +1,16 @@
|
||||
package exh.ui.migration
|
||||
|
||||
class MigrationStatus {
|
||||
companion object {
|
||||
val NOT_INITIALIZED = -1
|
||||
val COMPLETED = 0
|
||||
|
||||
//Migration process
|
||||
val NOTIFY_USER = 1
|
||||
val OPEN_BACKUP_MENU = 2
|
||||
val PERFORM_BACKUP = 3
|
||||
val FINALIZE_MIGRATION = 4
|
||||
|
||||
val MAX_MIGRATION_STEPS = 2
|
||||
}
|
||||
}
|
79
app/src/main/java/exh/ui/migration/UrlMigrator.kt
Executable file
79
app/src/main/java/exh/ui/migration/UrlMigrator.kt
Executable file
@ -0,0 +1,79 @@
|
||||
package exh.ui.migration
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import exh.isExSource
|
||||
import exh.isLewdSource
|
||||
import exh.metadata.MetadataHelper
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class UrlMigrator {
|
||||
private val db: DatabaseHelper by injectLazy()
|
||||
|
||||
private val prefs: PreferencesHelper by injectLazy()
|
||||
|
||||
private val metadataHelper: MetadataHelper by lazy { MetadataHelper() }
|
||||
|
||||
fun perform() {
|
||||
db.inTransaction {
|
||||
val dbMangas = db.getMangas()
|
||||
.executeAsBlocking()
|
||||
|
||||
//Find all EX mangas
|
||||
val qualifyingMangas = dbMangas.asSequence().filter {
|
||||
isLewdSource(it.source)
|
||||
}
|
||||
|
||||
val possibleDups = mutableListOf<Manga>()
|
||||
val badMangas = mutableListOf<Manga>()
|
||||
|
||||
qualifyingMangas.forEach {
|
||||
if(it.url.startsWith("g/")) //Missing slash at front so we are bad
|
||||
badMangas.add(it)
|
||||
else
|
||||
possibleDups.add(it)
|
||||
}
|
||||
|
||||
//Sort possible dups so we can use binary search on it
|
||||
possibleDups.sortBy { it.url }
|
||||
|
||||
badMangas.forEach { manga ->
|
||||
//Build fixed URL
|
||||
val urlWithSlash = "/" + manga.url
|
||||
//Fix metadata if required
|
||||
val metadata = metadataHelper.fetchEhMetadata(manga.url, isExSource(manga.source))
|
||||
metadata?.url?.let {
|
||||
if(it.startsWith("g/")) { //Check if metadata URL has no slash
|
||||
metadata.url = urlWithSlash //Fix it
|
||||
metadataHelper.writeGallery(metadata, manga.source) //Write new metadata to disk
|
||||
}
|
||||
}
|
||||
//If we have a dup (with the fixed url), use the dup instead
|
||||
val possibleDup = possibleDups.binarySearchBy(urlWithSlash, selector = { it.url })
|
||||
if(possibleDup >= 0) {
|
||||
//Make sure it is favorited if we are
|
||||
if(manga.favorite) {
|
||||
val dup = possibleDups[possibleDup]
|
||||
dup.favorite = true
|
||||
db.insertManga(dup).executeAsBlocking() //Update DB with changes
|
||||
}
|
||||
//Delete ourself (but the dup is still there)
|
||||
db.deleteManga(manga).executeAsBlocking()
|
||||
return@forEach
|
||||
}
|
||||
//No dup, correct URL and reinsert ourselves
|
||||
manga.url = urlWithSlash
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun tryMigration() {
|
||||
if(!prefs.hasPerformedURLMigration().getOrDefault()) {
|
||||
perform()
|
||||
prefs.hasPerformedURLMigration().set(true)
|
||||
}
|
||||
}
|
||||
}
|
10
app/src/main/java/exh/util/UriFilter.kt
Executable file
10
app/src/main/java/exh/util/UriFilter.kt
Executable file
@ -0,0 +1,10 @@
|
||||
package exh.util
|
||||
|
||||
import android.net.Uri
|
||||
|
||||
/**
|
||||
* Uri filter
|
||||
*/
|
||||
interface UriFilter {
|
||||
fun addToUri(builder: Uri.Builder)
|
||||
}
|
15
app/src/main/java/exh/util/UriGroup.kt
Executable file
15
app/src/main/java/exh/util/UriGroup.kt
Executable file
@ -0,0 +1,15 @@
|
||||
package exh.util
|
||||
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
||||
/**
|
||||
* UriGroup
|
||||
*/
|
||||
open class UriGroup<V>(name: String, state: List<V>) : Filter.Group<V>(name, state), UriFilter {
|
||||
override fun addToUri(builder: Uri.Builder) {
|
||||
state.forEach {
|
||||
if(it is UriFilter) it.addToUri(builder)
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user