Upstream merge

Internal permission change
Fix url adder
This commit is contained in:
NerdNumber9
2017-05-04 23:38:17 -04:00
parent 3f758d5981
commit 9dbb59f337
616 changed files with 4186 additions and 230 deletions

View 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

View 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)
}
}
}
}
}
}

View 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());
}*/
// }
}

View 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
}
}
}

View File

@ -0,0 +1,3 @@
package exh
operator fun StringBuilder.plusAssign(other: String) { append(other) }

View File

@ -0,0 +1,5 @@
package exh
import ru.lanwen.verbalregex.VerbalExpression
fun VerbalExpression.Builder.anyChar() = add(".")!!

View 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")!!
}

View 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) }
}

View 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"
}
}
}

View 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()}"
}
}
}

View 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
}
}
}

View 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()}"
}
}

View 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>
}

View File

@ -0,0 +1,7 @@
package exh.metadata.models
/**
* Simple tag model
*/
data class Tag(var name: String, var light: Boolean)

View File

@ -0,0 +1,3 @@
package exh.search
class MultiWildcard : TextComponent()

View File

@ -0,0 +1,4 @@
package exh.search
class Namespace(var namespace: String,
var tag: Text? = null) : QueryComponent()

View File

@ -0,0 +1,6 @@
package exh.search
open class QueryComponent {
var excluded = false
var exact = false
}

View 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
})
}

View File

@ -0,0 +1,3 @@
package exh.search
class SingleWildcard : TextComponent()

View File

@ -0,0 +1,3 @@
package exh.search
class StringTextComponent(val value: String) : TextComponent()

View 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!!
}
}

View File

@ -0,0 +1,3 @@
package exh.search
open class TextComponent

View 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()
}
}

View 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
}
}

View 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)
}
}

View 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)
}
}

View 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
}
}

View 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";
})()
"""
}
}

View 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()
}
}

View 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
}
}

View 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)
}
}
}

View File

@ -0,0 +1,10 @@
package exh.util
import android.net.Uri
/**
* Uri filter
*/
interface UriFilter {
fun addToUri(builder: Uri.Builder)
}

View 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)
}
}
}