mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-11-14 17:05:07 +01:00
commit
9a044e9037
@ -135,7 +135,6 @@ dependencies {
|
|||||||
val retrofit_version = "2.7.1"
|
val retrofit_version = "2.7.1"
|
||||||
implementation("com.squareup.retrofit2:retrofit:$retrofit_version")
|
implementation("com.squareup.retrofit2:retrofit:$retrofit_version")
|
||||||
implementation("com.squareup.retrofit2:converter-gson:$retrofit_version")
|
implementation("com.squareup.retrofit2:converter-gson:$retrofit_version")
|
||||||
implementation("com.squareup.retrofit2:adapter-rxjava:$retrofit_version")
|
|
||||||
|
|
||||||
// JSON
|
// JSON
|
||||||
implementation("com.google.code.gson:gson:2.8.6")
|
implementation("com.google.code.gson:gson:2.8.6")
|
||||||
|
@ -181,7 +181,7 @@ class BackupRestoreService : Service() {
|
|||||||
*/
|
*/
|
||||||
private suspend fun restoreBackup(uri: Uri) {
|
private suspend fun restoreBackup(uri: Uri) {
|
||||||
val reader = JsonReader(contentResolver.openInputStream(uri)!!.bufferedReader())
|
val reader = JsonReader(contentResolver.openInputStream(uri)!!.bufferedReader())
|
||||||
val json = JsonParser().parse(reader).asJsonObject
|
val json = JsonParser.parseReader(reader).asJsonObject
|
||||||
|
|
||||||
// Get parser version
|
// Get parser version
|
||||||
val version = json.get(VERSION)?.asInt ?: 1
|
val version = json.get(VERSION)?.asInt ?: 1
|
||||||
@ -296,16 +296,16 @@ class BackupRestoreService : Service() {
|
|||||||
* @param manga manga that needs updating.
|
* @param manga manga that needs updating.
|
||||||
* @param tracks list containing tracks from restore file.
|
* @param tracks list containing tracks from restore file.
|
||||||
*/
|
*/
|
||||||
private fun trackingFetch(manga: Manga, tracks: List<Track>) {
|
private suspend fun trackingFetch(manga: Manga, tracks: List<Track>) {
|
||||||
tracks.forEach { track ->
|
tracks.forEach { track ->
|
||||||
val service = trackManager.getService(track.sync_id)
|
val service = trackManager.getService(track.sync_id)
|
||||||
if (service != null && service.isLogged) {
|
if (service != null && service.isLogged) {
|
||||||
service.refresh(track)
|
try {
|
||||||
.doOnNext { db.insertTrack(it).executeAsBlocking() }
|
service.refresh(track)
|
||||||
.onErrorReturn {
|
db.insertTrack(track).executeAsBlocking()
|
||||||
errors.add("${manga.title} - ${it.message}")
|
}catch (e : Exception){
|
||||||
track
|
errors.add("${manga.title} - ${e.message}")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
errors.add("${manga.title} - ${service?.name} not logged in")
|
errors.add("${manga.title} - ${service?.name} not logged in")
|
||||||
val notLoggedIn = getString(R.string.not_logged_into, service?.name)
|
val notLoggedIn = getString(R.string.not_logged_into, service?.name)
|
||||||
|
@ -64,11 +64,11 @@ import java.util.concurrent.atomic.AtomicInteger
|
|||||||
* destroyed.
|
* destroyed.
|
||||||
*/
|
*/
|
||||||
class LibraryUpdateService(
|
class LibraryUpdateService(
|
||||||
val db: DatabaseHelper = Injekt.get(),
|
val db: DatabaseHelper = Injekt.get(),
|
||||||
val sourceManager: SourceManager = Injekt.get(),
|
val sourceManager: SourceManager = Injekt.get(),
|
||||||
val preferences: PreferencesHelper = Injekt.get(),
|
val preferences: PreferencesHelper = Injekt.get(),
|
||||||
val downloadManager: DownloadManager = Injekt.get(),
|
val downloadManager: DownloadManager = Injekt.get(),
|
||||||
val trackManager: TrackManager = Injekt.get()
|
val trackManager: TrackManager = Injekt.get()
|
||||||
) : Service() {
|
) : Service() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -81,7 +81,6 @@ class LibraryUpdateService(
|
|||||||
*/
|
*/
|
||||||
private var subscription: Subscription? = null
|
private var subscription: Subscription? = null
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pending intent of action that cancels the library update
|
* Pending intent of action that cancels the library update
|
||||||
*/
|
*/
|
||||||
@ -96,7 +95,7 @@ class LibraryUpdateService(
|
|||||||
BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)
|
BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var job:Job? = null
|
private var job: Job? = null
|
||||||
|
|
||||||
private val mangaToUpdate = mutableListOf<LibraryManga>()
|
private val mangaToUpdate = mutableListOf<LibraryManga>()
|
||||||
|
|
||||||
@ -108,14 +107,19 @@ class LibraryUpdateService(
|
|||||||
/**
|
/**
|
||||||
* Cached progress notification to avoid creating a lot.
|
* Cached progress notification to avoid creating a lot.
|
||||||
*/
|
*/
|
||||||
private val progressNotification by lazy { NotificationCompat.Builder(this, Notifications.CHANNEL_LIBRARY)
|
private val progressNotification by lazy {
|
||||||
|
NotificationCompat.Builder(this, Notifications.CHANNEL_LIBRARY)
|
||||||
.setContentTitle(getString(R.string.app_name))
|
.setContentTitle(getString(R.string.app_name))
|
||||||
.setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
.setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
||||||
.setLargeIcon(notificationBitmap)
|
.setLargeIcon(notificationBitmap)
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
.setOnlyAlertOnce(true)
|
.setOnlyAlertOnce(true)
|
||||||
.setColor(ContextCompat.getColor(this, R.color.colorAccent))
|
.setColor(ContextCompat.getColor(this, R.color.colorAccent))
|
||||||
.addAction(R.drawable.ic_clear_grey_24dp_img, getString(android.R.string.cancel), cancelIntent)
|
.addAction(
|
||||||
|
R.drawable.ic_clear_grey_24dp_img,
|
||||||
|
getString(android.R.string.cancel),
|
||||||
|
cancelIntent
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -172,8 +176,7 @@ class LibraryUpdateService(
|
|||||||
} else {
|
} else {
|
||||||
context.startForegroundService(intent)
|
context.startForegroundService(intent)
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
if (target == Target.CHAPTERS) category?.id?.let {
|
if (target == Target.CHAPTERS) category?.id?.let {
|
||||||
instance?.addCategory(it)
|
instance?.addCategory(it)
|
||||||
}
|
}
|
||||||
@ -190,7 +193,7 @@ class LibraryUpdateService(
|
|||||||
context.stopService(Intent(context, LibraryUpdateService::class.java))
|
context.stopService(Intent(context, LibraryUpdateService::class.java))
|
||||||
}
|
}
|
||||||
|
|
||||||
private var listener:LibraryServiceListener? = null
|
private var listener: LibraryServiceListener? = null
|
||||||
|
|
||||||
fun setListener(listener: LibraryServiceListener) {
|
fun setListener(listener: LibraryServiceListener) {
|
||||||
this.listener = listener
|
this.listener = listener
|
||||||
@ -212,7 +215,8 @@ class LibraryUpdateService(
|
|||||||
val selectedScheme = preferences.libraryUpdatePrioritization().getOrDefault()
|
val selectedScheme = preferences.libraryUpdatePrioritization().getOrDefault()
|
||||||
val mangas =
|
val mangas =
|
||||||
getMangaToUpdate(categoryId, Target.CHAPTERS).sortedWith(
|
getMangaToUpdate(categoryId, Target.CHAPTERS).sortedWith(
|
||||||
rankingScheme[selectedScheme])
|
rankingScheme[selectedScheme]
|
||||||
|
)
|
||||||
categoryIds.add(categoryId)
|
categoryIds.add(categoryId)
|
||||||
addManga(mangas)
|
addManga(mangas)
|
||||||
}
|
}
|
||||||
@ -228,9 +232,9 @@ class LibraryUpdateService(
|
|||||||
var listToUpdate = if (categoryId != -1) {
|
var listToUpdate = if (categoryId != -1) {
|
||||||
categoryIds.add(categoryId)
|
categoryIds.add(categoryId)
|
||||||
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
|
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
|
||||||
}
|
} else {
|
||||||
else {
|
val categoriesToUpdate =
|
||||||
val categoriesToUpdate = preferences.libraryUpdateCategories().getOrDefault().map(String::toInt)
|
preferences.libraryUpdateCategories().getOrDefault().map(String::toInt)
|
||||||
categoryIds.addAll(categoriesToUpdate)
|
categoryIds.addAll(categoriesToUpdate)
|
||||||
if (categoriesToUpdate.isNotEmpty())
|
if (categoriesToUpdate.isNotEmpty())
|
||||||
db.getLibraryMangas().executeAsBlocking()
|
db.getLibraryMangas().executeAsBlocking()
|
||||||
@ -259,7 +263,8 @@ class LibraryUpdateService(
|
|||||||
super.onCreate()
|
super.onCreate()
|
||||||
startForeground(Notifications.ID_LIBRARY_PROGRESS, progressNotification.build())
|
startForeground(Notifications.ID_LIBRARY_PROGRESS, progressNotification.build())
|
||||||
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
|
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
|
||||||
PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock")
|
PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock"
|
||||||
|
)
|
||||||
wakeLock.acquire(TimeUnit.MINUTES.toMillis(30))
|
wakeLock.acquire(TimeUnit.MINUTES.toMillis(30))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -297,7 +302,7 @@ class LibraryUpdateService(
|
|||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
if (intent == null) return START_NOT_STICKY
|
if (intent == null) return START_NOT_STICKY
|
||||||
val target = intent.getSerializableExtra(KEY_TARGET) as? Target
|
val target = intent.getSerializableExtra(KEY_TARGET) as? Target
|
||||||
?: return START_NOT_STICKY
|
?: return START_NOT_STICKY
|
||||||
|
|
||||||
// Unsubscribe from any previous subscription if needed.
|
// Unsubscribe from any previous subscription if needed.
|
||||||
subscription?.unsubscribe()
|
subscription?.unsubscribe()
|
||||||
@ -307,41 +312,44 @@ class LibraryUpdateService(
|
|||||||
val mangaList =
|
val mangaList =
|
||||||
getMangaToUpdate(intent, target).sortedWith(rankingScheme[selectedScheme])
|
getMangaToUpdate(intent, target).sortedWith(rankingScheme[selectedScheme])
|
||||||
// Update favorite manga. Destroy service when completed or in case of an error.
|
// Update favorite manga. Destroy service when completed or in case of an error.
|
||||||
if (target == Target.CHAPTERS) {
|
if (target == Target.DETAILS) {
|
||||||
updateChapters(mangaList, startId)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Update either chapter list or manga details.
|
// Update either chapter list or manga details.
|
||||||
subscription = Observable.defer {
|
subscription = Observable.defer {
|
||||||
when (target) {
|
updateDetails(mangaList)
|
||||||
Target.DETAILS -> updateDetails(mangaList)
|
|
||||||
else -> updateTrackings(mangaList)
|
|
||||||
}
|
|
||||||
}.subscribeOn(Schedulers.io()).subscribe({}, {
|
}.subscribeOn(Schedulers.io()).subscribe({}, {
|
||||||
Timber.e(it)
|
Timber.e(it)
|
||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
}, {
|
}, {
|
||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
launchTarget(target, mangaList, startId)
|
||||||
}
|
}
|
||||||
return START_REDELIVER_INTENT
|
return START_REDELIVER_INTENT
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateChapters(mangaToAdd: List<LibraryManga>, startId: Int) {
|
private fun launchTarget(target: Target, mangaToAdd: List<LibraryManga>, startId: Int) {
|
||||||
val handler = CoroutineExceptionHandler { _, exception ->
|
val handler = CoroutineExceptionHandler { _, exception ->
|
||||||
Timber.e(exception)
|
Timber.e(exception)
|
||||||
// Boolean to determine if user wants to automatically download new chapters.
|
|
||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
}
|
}
|
||||||
job = GlobalScope.launch(handler) {
|
if (target == Target.CHAPTERS) {
|
||||||
updateChaptersJob(mangaToAdd)
|
job = GlobalScope.launch(handler) {
|
||||||
|
updateChaptersJob(mangaToAdd)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
job = GlobalScope.launch(handler) {
|
||||||
|
updateTrackings(mangaToAdd)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
job?.invokeOnCompletion { stopSelf(startId) }
|
job?.invokeOnCompletion { stopSelf(startId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateChaptersJob(mangaToAdd: List<LibraryManga>) {
|
private suspend fun updateChaptersJob(mangaToAdd: List<LibraryManga>) {
|
||||||
// List containing categories that get included in downloads.
|
// List containing categories that get included in downloads.
|
||||||
val categoriesToDownload = preferences.downloadNewCategories().getOrDefault().map(String::toInt)
|
val categoriesToDownload =
|
||||||
|
preferences.downloadNewCategories().getOrDefault().map(String::toInt)
|
||||||
// Boolean to determine if user wants to automatically download new chapters.
|
// Boolean to determine if user wants to automatically download new chapters.
|
||||||
val downloadNew = preferences.downloadNew().getOrDefault()
|
val downloadNew = preferences.downloadNew().getOrDefault()
|
||||||
// Boolean to determine if DownloadManager has downloads
|
// Boolean to determine if DownloadManager has downloads
|
||||||
@ -352,7 +360,7 @@ class LibraryUpdateService(
|
|||||||
mangaToUpdate.addAll(mangaToAdd)
|
mangaToUpdate.addAll(mangaToAdd)
|
||||||
while (count < mangaToUpdate.size) {
|
while (count < mangaToUpdate.size) {
|
||||||
val shouldDownload = (downloadNew && (categoriesToDownload.isEmpty() ||
|
val shouldDownload = (downloadNew && (categoriesToDownload.isEmpty() ||
|
||||||
mangaToUpdate[count].category in categoriesToDownload))
|
mangaToUpdate[count].category in categoriesToDownload))
|
||||||
if (updateMangaChapters(mangaToUpdate[count], count, shouldDownload)) {
|
if (updateMangaChapters(mangaToUpdate[count], count, shouldDownload)) {
|
||||||
hasDownloads = true
|
hasDownloads = true
|
||||||
}
|
}
|
||||||
@ -370,8 +378,7 @@ class LibraryUpdateService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.subscribeOn(Schedulers.io()).subscribe {}
|
.subscribeOn(Schedulers.io()).subscribe {}
|
||||||
}
|
} else if (downloadNew && hasDownloads) {
|
||||||
else if (downloadNew && hasDownloads) {
|
|
||||||
DownloadService.start(this)
|
DownloadService.start(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -379,8 +386,12 @@ class LibraryUpdateService(
|
|||||||
cancelProgressNotification()
|
cancelProgressNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateMangaChapters(manga: LibraryManga, progess: Int, shouldDownload: Boolean):
|
private suspend fun updateMangaChapters(
|
||||||
Boolean {
|
manga: LibraryManga,
|
||||||
|
progess: Int,
|
||||||
|
shouldDownload: Boolean
|
||||||
|
):
|
||||||
|
Boolean {
|
||||||
try {
|
try {
|
||||||
var hasDownloads = false
|
var hasDownloads = false
|
||||||
if (job?.isCancelled == true) {
|
if (job?.isCancelled == true) {
|
||||||
@ -389,7 +400,7 @@ class LibraryUpdateService(
|
|||||||
showProgressNotification(manga, progess, mangaToUpdate.size)
|
showProgressNotification(manga, progess, mangaToUpdate.size)
|
||||||
val source = sourceManager.get(manga.source) as? HttpSource ?: return false
|
val source = sourceManager.get(manga.source) as? HttpSource ?: return false
|
||||||
val fetchedChapters = withContext(Dispatchers.IO) {
|
val fetchedChapters = withContext(Dispatchers.IO) {
|
||||||
source.fetchChapterList(manga).toBlocking().single()
|
source.fetchChapterList(manga).toBlocking().single()
|
||||||
} ?: emptyList()
|
} ?: emptyList()
|
||||||
if (fetchedChapters.isNotEmpty()) {
|
if (fetchedChapters.isNotEmpty()) {
|
||||||
val newChapters = syncChaptersWithSource(db, fetchedChapters, manga, source)
|
val newChapters = syncChaptersWithSource(db, fetchedChapters, manga, source)
|
||||||
@ -406,8 +417,7 @@ class LibraryUpdateService(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
return hasDownloads
|
return hasDownloads
|
||||||
}
|
} catch (e: Exception) {
|
||||||
catch (e: Exception) {
|
|
||||||
Timber.e("Failed updating: ${manga.title}: $e")
|
Timber.e("Failed updating: ${manga.title}: $e")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -433,7 +443,7 @@ class LibraryUpdateService(
|
|||||||
fun updateManga(manga: Manga): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
fun updateManga(manga: Manga): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
||||||
val source = sourceManager.get(manga.source) as? HttpSource ?: return Observable.empty()
|
val source = sourceManager.get(manga.source) as? HttpSource ?: return Observable.empty()
|
||||||
return source.fetchChapterList(manga)
|
return source.fetchChapterList(manga)
|
||||||
.map { syncChaptersWithSource(db, it, manga, source) }
|
.map { syncChaptersWithSource(db, it, manga, source) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -449,62 +459,57 @@ class LibraryUpdateService(
|
|||||||
|
|
||||||
// Emit each manga and update it sequentially.
|
// Emit each manga and update it sequentially.
|
||||||
return Observable.from(mangaToUpdate)
|
return Observable.from(mangaToUpdate)
|
||||||
// Notify manga that will update.
|
// Notify manga that will update.
|
||||||
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) }
|
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) }
|
||||||
// Update the details of the manga.
|
// Update the details of the manga.
|
||||||
.concatMap { manga ->
|
.concatMap { manga ->
|
||||||
val source = sourceManager.get(manga.source) as? HttpSource
|
val source = sourceManager.get(manga.source) as? HttpSource
|
||||||
?: return@concatMap Observable.empty<LibraryManga>()
|
?: return@concatMap Observable.empty<LibraryManga>()
|
||||||
source.fetchMangaDetails(manga)
|
source.fetchMangaDetails(manga)
|
||||||
.map { networkManga ->
|
.map { networkManga ->
|
||||||
val thumbnailUrl = manga.thumbnail_url
|
val thumbnailUrl = manga.thumbnail_url
|
||||||
manga.copyFrom(networkManga)
|
manga.copyFrom(networkManga)
|
||||||
db.insertManga(manga).executeAsBlocking()
|
db.insertManga(manga).executeAsBlocking()
|
||||||
if (thumbnailUrl != networkManga.thumbnail_url)
|
if (thumbnailUrl != networkManga.thumbnail_url)
|
||||||
MangaImpl.setLastCoverFetch(manga.id!!, Date().time)
|
MangaImpl.setLastCoverFetch(manga.id!!, Date().time)
|
||||||
manga
|
manga
|
||||||
}
|
}
|
||||||
.onErrorReturn { manga }
|
.onErrorReturn { manga }
|
||||||
}
|
}
|
||||||
.doOnCompleted {
|
.doOnCompleted {
|
||||||
cancelProgressNotification()
|
cancelProgressNotification()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method that updates the metadata of the connected tracking services. It's called in a
|
* Method that updates the metadata of the connected tracking services. It's called in a
|
||||||
* background thread, so it's safe to do heavy operations or network calls here.
|
* background thread, so it's safe to do heavy operations or network calls here.
|
||||||
*/
|
*/
|
||||||
private fun updateTrackings(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
|
|
||||||
|
private suspend fun updateTrackings(mangaToUpdate: List<LibraryManga>) {
|
||||||
// Initialize the variables holding the progress of the updates.
|
// Initialize the variables holding the progress of the updates.
|
||||||
var count = 0
|
var count = 0
|
||||||
|
|
||||||
val loggedServices = trackManager.services.filter { it.isLogged }
|
val loggedServices = trackManager.services.filter { it.isLogged }
|
||||||
|
|
||||||
// Emit each manga and update it sequentially.
|
mangaToUpdate.forEach { manga ->
|
||||||
return Observable.from(mangaToUpdate)
|
showProgressNotification(manga, count++, mangaToUpdate.size)
|
||||||
// Notify manga that will update.
|
|
||||||
.doOnNext { showProgressNotification(it, count++, mangaToUpdate.size) }
|
|
||||||
// Update the tracking details.
|
|
||||||
.concatMap { manga ->
|
|
||||||
val tracks = db.getTracks(manga).executeAsBlocking()
|
|
||||||
|
|
||||||
Observable.from(tracks)
|
val tracks = db.getTracks(manga).executeAsBlocking()
|
||||||
.concatMap { track ->
|
|
||||||
val service = trackManager.getService(track.sync_id)
|
tracks.forEach { track ->
|
||||||
if (service != null && service in loggedServices) {
|
val service = trackManager.getService(track.sync_id)
|
||||||
service.refresh(track)
|
if (service != null && service in loggedServices) {
|
||||||
.doOnNext { db.insertTrack(it).executeAsBlocking() }
|
try {
|
||||||
.onErrorReturn { track }
|
service.refresh(track)
|
||||||
} else {
|
db.insertTrack(track).executeAsBlocking()
|
||||||
Observable.empty()
|
} catch (e: Exception) {
|
||||||
}
|
Timber.e(e)
|
||||||
}
|
}
|
||||||
.map { manga }
|
|
||||||
}
|
|
||||||
.doOnCompleted {
|
|
||||||
cancelProgressNotification()
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cancelProgressNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -515,10 +520,12 @@ class LibraryUpdateService(
|
|||||||
* @param total the total progress.
|
* @param total the total progress.
|
||||||
*/
|
*/
|
||||||
private fun showProgressNotification(manga: Manga, current: Int, total: Int) {
|
private fun showProgressNotification(manga: Manga, current: Int, total: Int) {
|
||||||
notificationManager.notify(Notifications.ID_LIBRARY_PROGRESS, progressNotification
|
notificationManager.notify(
|
||||||
|
Notifications.ID_LIBRARY_PROGRESS, progressNotification
|
||||||
.setContentTitle(manga.currentTitle())
|
.setContentTitle(manga.currentTitle())
|
||||||
.setProgress(total, current, false)
|
.setProgress(total, current, false)
|
||||||
.build())
|
.build()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -539,15 +546,17 @@ class LibraryUpdateService(
|
|||||||
.asBitmap().load(manga).dontTransform().centerCrop().circleCrop()
|
.asBitmap().load(manga).dontTransform().centerCrop().circleCrop()
|
||||||
.override(256, 256).submit().get()
|
.override(256, 256).submit().get()
|
||||||
setLargeIcon(icon)
|
setLargeIcon(icon)
|
||||||
|
} catch (e: Exception) {
|
||||||
}
|
}
|
||||||
catch (e: Exception) { }
|
|
||||||
setGroupAlertBehavior(GROUP_ALERT_SUMMARY)
|
setGroupAlertBehavior(GROUP_ALERT_SUMMARY)
|
||||||
setContentTitle(manga.currentTitle())
|
setContentTitle(manga.currentTitle())
|
||||||
color = ContextCompat.getColor(this@LibraryUpdateService, R.color.colorAccent)
|
color = ContextCompat.getColor(this@LibraryUpdateService, R.color.colorAccent)
|
||||||
val chaptersNames = if (chapterNames.size > 5) {
|
val chaptersNames = if (chapterNames.size > 5) {
|
||||||
"${chapterNames.take(4).joinToString(", ")}, " +
|
"${chapterNames.take(4).joinToString(", ")}, " +
|
||||||
resources.getQuantityString(R.plurals.notification_and_n_more,
|
resources.getQuantityString(
|
||||||
(chapterNames.size - 4), (chapterNames.size - 4))
|
R.plurals.notification_and_n_more,
|
||||||
|
(chapterNames.size - 4), (chapterNames.size - 4)
|
||||||
|
)
|
||||||
} else chapterNames.joinToString(", ")
|
} else chapterNames.joinToString(", ")
|
||||||
setContentText(chaptersNames)
|
setContentText(chaptersNames)
|
||||||
setStyle(NotificationCompat.BigTextStyle().bigText(chaptersNames))
|
setStyle(NotificationCompat.BigTextStyle().bigText(chaptersNames))
|
||||||
@ -558,41 +567,57 @@ class LibraryUpdateService(
|
|||||||
this@LibraryUpdateService, manga, chapters.first()
|
this@LibraryUpdateService, manga, chapters.first()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
addAction(R.drawable.ic_glasses_black_24dp, getString(R.string.action_mark_as_read),
|
addAction(
|
||||||
NotificationReceiver.markAsReadPendingBroadcast(this@LibraryUpdateService,
|
R.drawable.ic_glasses_black_24dp, getString(R.string.action_mark_as_read),
|
||||||
manga, chapters, Notifications.ID_NEW_CHAPTERS))
|
NotificationReceiver.markAsReadPendingBroadcast(
|
||||||
addAction(R.drawable.ic_book_white_24dp, getString(R.string.action_view_chapters),
|
this@LibraryUpdateService,
|
||||||
NotificationReceiver.openChapterPendingActivity(this@LibraryUpdateService,
|
manga, chapters, Notifications.ID_NEW_CHAPTERS
|
||||||
manga, Notifications.ID_NEW_CHAPTERS))
|
)
|
||||||
|
)
|
||||||
|
addAction(
|
||||||
|
R.drawable.ic_book_white_24dp, getString(R.string.action_view_chapters),
|
||||||
|
NotificationReceiver.openChapterPendingActivity(
|
||||||
|
this@LibraryUpdateService,
|
||||||
|
manga, Notifications.ID_NEW_CHAPTERS
|
||||||
|
)
|
||||||
|
)
|
||||||
setAutoCancel(true)
|
setAutoCancel(true)
|
||||||
}, manga.id.hashCode()))
|
}, manga.id.hashCode()))
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationManagerCompat.from(this).apply {
|
NotificationManagerCompat.from(this).apply {
|
||||||
|
|
||||||
notify(Notifications.ID_NEW_CHAPTERS, notification(Notifications.CHANNEL_NEW_CHAPTERS) {
|
notify(
|
||||||
setSmallIcon(R.drawable.ic_tachi)
|
Notifications.ID_NEW_CHAPTERS,
|
||||||
setLargeIcon(notificationBitmap)
|
notification(Notifications.CHANNEL_NEW_CHAPTERS) {
|
||||||
setContentTitle(getString(R.string.notification_new_chapters))
|
setSmallIcon(R.drawable.ic_tachi)
|
||||||
color = ContextCompat.getColor(applicationContext, R.color.colorAccent)
|
setLargeIcon(notificationBitmap)
|
||||||
if (updates.size > 1) {
|
setContentTitle(getString(R.string.notification_new_chapters))
|
||||||
setContentText(resources.getQuantityString(R.plurals
|
color = ContextCompat.getColor(applicationContext, R.color.colorAccent)
|
||||||
.notification_new_chapters_text,
|
if (updates.size > 1) {
|
||||||
updates.size, updates.size))
|
setContentText(
|
||||||
setStyle(NotificationCompat.BigTextStyle().bigText(updates.keys.joinToString("\n") {
|
resources.getQuantityString(
|
||||||
it.currentTitle().chop(45)
|
R.plurals
|
||||||
}))
|
.notification_new_chapters_text,
|
||||||
}
|
updates.size, updates.size
|
||||||
else {
|
)
|
||||||
setContentText(updates.keys.first().currentTitle().chop(45))
|
)
|
||||||
}
|
setStyle(
|
||||||
priority = NotificationCompat.PRIORITY_HIGH
|
NotificationCompat.BigTextStyle()
|
||||||
setGroup(Notifications.GROUP_NEW_CHAPTERS)
|
.bigText(updates.keys.joinToString("\n") {
|
||||||
setGroupAlertBehavior(GROUP_ALERT_SUMMARY)
|
it.currentTitle().chop(45)
|
||||||
setGroupSummary(true)
|
})
|
||||||
setContentIntent(getNotificationIntent())
|
)
|
||||||
setAutoCancel(true)
|
} else {
|
||||||
})
|
setContentText(updates.keys.first().currentTitle().chop(45))
|
||||||
|
}
|
||||||
|
priority = NotificationCompat.PRIORITY_HIGH
|
||||||
|
setGroup(Notifications.GROUP_NEW_CHAPTERS)
|
||||||
|
setGroupAlertBehavior(GROUP_ALERT_SUMMARY)
|
||||||
|
setGroupSummary(true)
|
||||||
|
setContentIntent(getNotificationIntent())
|
||||||
|
setAutoCancel(true)
|
||||||
|
})
|
||||||
|
|
||||||
notifications.forEach {
|
notifications.forEach {
|
||||||
notify(it.second, it.first)
|
notify(it.second, it.first)
|
||||||
|
@ -3,11 +3,11 @@ package eu.kanade.tachiyomi.data.track
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||||
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
|
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
|
||||||
import eu.kanade.tachiyomi.data.track.myanimelist.Myanimelist
|
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList
|
||||||
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
|
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
|
||||||
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
|
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
|
||||||
|
|
||||||
class TrackManager(private val context: Context) {
|
class TrackManager(context: Context) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val MYANIMELIST = 1
|
const val MYANIMELIST = 1
|
||||||
@ -17,7 +17,7 @@ class TrackManager(private val context: Context) {
|
|||||||
const val BANGUMI = 5
|
const val BANGUMI = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
val myAnimeList = Myanimelist(context, MYANIMELIST)
|
val myAnimeList = MyAnimeList(context, MYANIMELIST)
|
||||||
|
|
||||||
val aniList = Anilist(context, ANILIST)
|
val aniList = Anilist(context, ANILIST)
|
||||||
|
|
||||||
|
@ -7,8 +7,6 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
|||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import rx.Completable
|
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
abstract class TrackService(val id: Int) {
|
abstract class TrackService(val id: Int) {
|
||||||
@ -39,17 +37,15 @@ abstract class TrackService(val id: Int) {
|
|||||||
|
|
||||||
abstract fun displayScore(track: Track): String
|
abstract fun displayScore(track: Track): String
|
||||||
|
|
||||||
abstract fun add(track: Track): Observable<Track>
|
abstract suspend fun update(track: Track): Track
|
||||||
|
|
||||||
abstract fun update(track: Track): Observable<Track>
|
abstract suspend fun bind(track: Track): Track
|
||||||
|
|
||||||
abstract fun bind(track: Track): Observable<Track>
|
abstract suspend fun search(query: String): List<TrackSearch>
|
||||||
|
|
||||||
abstract fun search(query: String): Observable<List<TrackSearch>>
|
abstract suspend fun refresh(track: Track): Track
|
||||||
|
|
||||||
abstract fun refresh(track: Track): Observable<Track>
|
abstract suspend fun login(username: String, password: String): Boolean
|
||||||
|
|
||||||
abstract fun login(username: String, password: String): Completable
|
|
||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
open fun logout() {
|
open fun logout() {
|
||||||
|
@ -8,30 +8,11 @@ import eu.kanade.tachiyomi.data.database.models.Track
|
|||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import rx.Completable
|
import timber.log.Timber
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val READING = 1
|
|
||||||
const val COMPLETED = 2
|
|
||||||
const val PAUSED = 3
|
|
||||||
const val DROPPED = 4
|
|
||||||
const val PLANNING = 5
|
|
||||||
const val REPEATING = 6
|
|
||||||
|
|
||||||
const val DEFAULT_STATUS = READING
|
|
||||||
const val DEFAULT_SCORE = 0
|
|
||||||
|
|
||||||
const val POINT_100 = "POINT_100"
|
|
||||||
const val POINT_10 = "POINT_10"
|
|
||||||
const val POINT_10_DECIMAL = "POINT_10_DECIMAL"
|
|
||||||
const val POINT_5 = "POINT_5"
|
|
||||||
const val POINT_3 = "POINT_3"
|
|
||||||
}
|
|
||||||
|
|
||||||
override val name = "AniList"
|
override val name = "AniList"
|
||||||
|
|
||||||
private val gson: Gson by injectLazy()
|
private val gson: Gson by injectLazy()
|
||||||
@ -56,9 +37,7 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
|
|
||||||
override fun getLogoColor() = Color.rgb(18, 25, 35)
|
override fun getLogoColor() = Color.rgb(18, 25, 35)
|
||||||
|
|
||||||
override fun getStatusList(): List<Int> {
|
override fun getStatusList() = listOf(READING, PLANNING, COMPLETED, REPEATING, PAUSED, DROPPED)
|
||||||
return listOf(READING, PLANNING, COMPLETED, REPEATING, PAUSED, DROPPED)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getStatus(status: Int): String = with(context) {
|
override fun getStatus(status: Int): String = with(context) {
|
||||||
when (status) {
|
when (status) {
|
||||||
@ -95,13 +74,13 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
// 100 point
|
// 100 point
|
||||||
POINT_100 -> index.toFloat()
|
POINT_100 -> index.toFloat()
|
||||||
// 5 stars
|
// 5 stars
|
||||||
POINT_5 -> when {
|
POINT_5 -> when (index) {
|
||||||
index == 0 -> 0f
|
0 -> 0f
|
||||||
else -> index * 20f - 10f
|
else -> index * 20f - 10f
|
||||||
}
|
}
|
||||||
// Smiley
|
// Smiley
|
||||||
POINT_3 -> when {
|
POINT_3 -> when (index) {
|
||||||
index == 0 -> 0f
|
0 -> 0f
|
||||||
else -> index * 25f + 10f
|
else -> index * 25f + 10f
|
||||||
}
|
}
|
||||||
// 10 point decimal
|
// 10 point decimal
|
||||||
@ -114,8 +93,8 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
val score = track.score
|
val score = track.score
|
||||||
|
|
||||||
return when (scorePreference.getOrDefault()) {
|
return when (scorePreference.getOrDefault()) {
|
||||||
POINT_5 -> when {
|
POINT_5 -> when (score) {
|
||||||
score == 0f -> "0 ★"
|
0f -> "0 ★"
|
||||||
else -> "${((score + 10) / 20).toInt()} ★"
|
else -> "${((score + 10) / 20).toInt()} ★"
|
||||||
}
|
}
|
||||||
POINT_3 -> when {
|
POINT_3 -> when {
|
||||||
@ -128,68 +107,62 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun add(track: Track): Observable<Track> {
|
override suspend fun update(track: Track): Track {
|
||||||
return api.addLibManga(track)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun update(track: Track): Observable<Track> {
|
|
||||||
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||||
track.status = COMPLETED
|
track.status = COMPLETED
|
||||||
}
|
}
|
||||||
// If user was using API v1 fetch library_id
|
// If user was using API v1 fetch library_id
|
||||||
if (track.library_id == null || track.library_id!! == 0L){
|
if (track.library_id == null || track.library_id!! == 0L) {
|
||||||
return api.findLibManga(track, getUsername().toInt()).flatMap {
|
val libManga = api.findLibManga(track, getUsername().toInt())
|
||||||
if (it == null) {
|
?: throw Exception("$track not found on user library")
|
||||||
throw Exception("$track not found on user library")
|
|
||||||
}
|
track.library_id = libManga.library_id
|
||||||
track.library_id = it.library_id
|
|
||||||
api.updateLibManga(track)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return api.updateLibManga(track)
|
return api.updateLibraryManga(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun bind(track: Track): Observable<Track> {
|
override suspend fun bind(track: Track): Track {
|
||||||
return api.findLibManga(track, getUsername().toInt())
|
val remoteTrack = api.findLibManga(track, getUsername().toInt())
|
||||||
.flatMap { remoteTrack ->
|
|
||||||
if (remoteTrack != null) {
|
return if (remoteTrack != null) {
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.library_id = remoteTrack.library_id
|
track.library_id = remoteTrack.library_id
|
||||||
update(track)
|
update(track)
|
||||||
} else {
|
} else {
|
||||||
// Set default fields if it's not found in the list
|
// Set default fields if it's not found in the list
|
||||||
track.score = DEFAULT_SCORE.toFloat()
|
track.score = DEFAULT_SCORE.toFloat()
|
||||||
track.status = DEFAULT_STATUS
|
track.status = DEFAULT_STATUS
|
||||||
add(track)
|
api.addLibManga(track)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun search(query: String): Observable<List<TrackSearch>> {
|
override suspend fun search(query: String) = api.search(query)
|
||||||
return api.search(query)
|
|
||||||
|
override suspend fun refresh(track: Track): Track {
|
||||||
|
val remoteTrack = api.getLibManga(track, getUsername().toInt())
|
||||||
|
track.copyPersonalFrom(remoteTrack)
|
||||||
|
track.total_chapters = remoteTrack.total_chapters
|
||||||
|
return track
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun refresh(track: Track): Observable<Track> {
|
override suspend fun login(username: String, password: String) = login(password)
|
||||||
return api.getLibManga(track, getUsername().toInt())
|
|
||||||
.map { remoteTrack ->
|
|
||||||
track.copyPersonalFrom(remoteTrack)
|
|
||||||
track.total_chapters = remoteTrack.total_chapters
|
|
||||||
track
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun login(username: String, password: String) = login(password)
|
suspend fun login(token: String): Boolean {
|
||||||
|
|
||||||
fun login(token: String): Completable {
|
|
||||||
val oauth = api.createOAuth(token)
|
val oauth = api.createOAuth(token)
|
||||||
interceptor.setAuth(oauth)
|
interceptor.setAuth(oauth)
|
||||||
return api.getCurrentUser().map { (username, scoreType) ->
|
|
||||||
scorePreference.set(scoreType)
|
return try {
|
||||||
saveCredentials(username.toString(), oauth.access_token)
|
val currentUser = api.getCurrentUser()
|
||||||
}.doOnError{
|
scorePreference.set(currentUser.second)
|
||||||
logout()
|
saveCredentials(currentUser.first.toString(), oauth.access_token)
|
||||||
}.toCompletable()
|
true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
logout()
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun logout() {
|
override fun logout() {
|
||||||
@ -206,9 +179,29 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
return try {
|
return try {
|
||||||
gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java)
|
gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val READING = 1
|
||||||
|
const val COMPLETED = 2
|
||||||
|
const val PAUSED = 3
|
||||||
|
const val DROPPED = 4
|
||||||
|
const val PLANNING = 5
|
||||||
|
const val REPEATING = 6
|
||||||
|
|
||||||
|
const val DEFAULT_STATUS = READING
|
||||||
|
const val DEFAULT_SCORE = 0
|
||||||
|
|
||||||
|
const val POINT_100 = "POINT_100"
|
||||||
|
const val POINT_10 = "POINT_10"
|
||||||
|
const val POINT_10_DECIMAL = "POINT_10_DECIMAL"
|
||||||
|
const val POINT_5 = "POINT_5"
|
||||||
|
const val POINT_3 = "POINT_3"
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,25 +11,209 @@ import com.google.gson.JsonObject
|
|||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.jsonType
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.MediaType
|
import okhttp3.MediaType
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody
|
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import rx.Observable
|
import okhttp3.Response
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||||
|
|
||||||
private val parser = JsonParser()
|
|
||||||
private val jsonMime = "application/json; charset=utf-8".toMediaTypeOrNull()
|
|
||||||
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||||
|
|
||||||
fun addLibManga(track: Track): Observable<Track> {
|
suspend fun addLibManga(track: Track): Track {
|
||||||
val query = """
|
return withContext(Dispatchers.IO) {
|
||||||
|
|
||||||
|
val variables = jsonObject(
|
||||||
|
"mangaId" to track.media_id,
|
||||||
|
"progress" to track.last_chapter_read,
|
||||||
|
"status" to track.toAnilistStatus()
|
||||||
|
)
|
||||||
|
val payload = jsonObject(
|
||||||
|
"query" to addToLibraryQuery(),
|
||||||
|
"variables" to variables
|
||||||
|
)
|
||||||
|
val body = payload.toString().toRequestBody(MediaType.jsonType())
|
||||||
|
val request = Request.Builder().url(apiUrl).post(body).build()
|
||||||
|
|
||||||
|
val netResponse = authClient.newCall(request).execute()
|
||||||
|
|
||||||
|
val responseBody = netResponse.body?.string().orEmpty()
|
||||||
|
netResponse.close()
|
||||||
|
if (responseBody.isEmpty()) {
|
||||||
|
throw Exception("Null Response")
|
||||||
|
}
|
||||||
|
val response = JsonParser.parseString(responseBody).obj
|
||||||
|
track.library_id = response["data"]["SaveMediaListEntry"]["id"].asLong
|
||||||
|
track
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateLibraryManga(track: Track): Track {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
val variables = jsonObject(
|
||||||
|
"listId" to track.library_id,
|
||||||
|
"progress" to track.last_chapter_read,
|
||||||
|
"status" to track.toAnilistStatus(),
|
||||||
|
"score" to track.score.toInt()
|
||||||
|
)
|
||||||
|
val payload = jsonObject(
|
||||||
|
"query" to updateInLibraryQuery(),
|
||||||
|
"variables" to variables
|
||||||
|
)
|
||||||
|
val body = payload.toString().toRequestBody(MediaType.jsonType())
|
||||||
|
val request = Request.Builder().url(apiUrl).post(body).build()
|
||||||
|
val response = authClient.newCall(request).execute()
|
||||||
|
|
||||||
|
track
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun search(search: String): List<TrackSearch> {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
val variables = jsonObject(
|
||||||
|
"query" to search
|
||||||
|
)
|
||||||
|
val payload = jsonObject(
|
||||||
|
"query" to searchQuery(),
|
||||||
|
"variables" to variables
|
||||||
|
)
|
||||||
|
val body = payload.toString().toRequestBody(MediaType.jsonType())
|
||||||
|
val request = Request.Builder().url(apiUrl).post(body).build()
|
||||||
|
val netResponse = authClient.newCall(request).execute()
|
||||||
|
val response = responseToJson(netResponse)
|
||||||
|
|
||||||
|
val media = response["data"]!!.obj["Page"].obj["mediaList"].array
|
||||||
|
val entries = media.map { jsonToALManga(it.obj) }
|
||||||
|
entries.map { it.toTrack() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun findLibManga(track: Track, userid: Int): Track? {
|
||||||
|
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
val variables = jsonObject(
|
||||||
|
"id" to userid,
|
||||||
|
"manga_id" to track.media_id
|
||||||
|
)
|
||||||
|
val payload = jsonObject(
|
||||||
|
"query" to findLibraryMangaQuery(),
|
||||||
|
"variables" to variables
|
||||||
|
)
|
||||||
|
val body = payload.toString().toRequestBody(MediaType.jsonType())
|
||||||
|
val request = Request.Builder().url(apiUrl).post(body).build()
|
||||||
|
val result = authClient.newCall(request).execute()
|
||||||
|
|
||||||
|
result.let { resp ->
|
||||||
|
val response = responseToJson(resp)
|
||||||
|
val media = response["data"]!!.obj["Page"].obj["mediaList"].array
|
||||||
|
val entries = media.map { jsonToALUserManga(it.obj) }
|
||||||
|
|
||||||
|
entries.firstOrNull()?.toTrack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getLibManga(track: Track, userid: Int): Track {
|
||||||
|
val remoteTrack = findLibManga(track, userid)
|
||||||
|
if (remoteTrack == null) {
|
||||||
|
throw Exception("Could not find manga")
|
||||||
|
} else {
|
||||||
|
return remoteTrack
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createOAuth(token: String): OAuth {
|
||||||
|
return OAuth(
|
||||||
|
token,
|
||||||
|
"Bearer",
|
||||||
|
System.currentTimeMillis() + TimeUnit.DAYS.toMillis(365),
|
||||||
|
TimeUnit.DAYS.toMillis(365)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getCurrentUser(): Pair<Int, String> {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
val payload = jsonObject(
|
||||||
|
"query" to currentUserQuery()
|
||||||
|
)
|
||||||
|
val body = payload.toString().toRequestBody(MediaType.jsonType())
|
||||||
|
val request = Request.Builder().url(apiUrl).post(body).build()
|
||||||
|
val netResponse = authClient.newCall(request).execute()
|
||||||
|
|
||||||
|
val response = responseToJson(netResponse)
|
||||||
|
val viewer = response["data"]!!.obj["Viewer"].obj
|
||||||
|
|
||||||
|
Pair(viewer["id"].asInt, viewer["mediaListOptions"]["scoreFormat"].asString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun responseToJson(netResponse: Response): JsonObject {
|
||||||
|
val responseBody = netResponse.body?.string().orEmpty()
|
||||||
|
|
||||||
|
if (responseBody.isEmpty()) {
|
||||||
|
throw Exception("Null Response")
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonParser.parseString(responseBody).obj
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun jsonToALManga(struct: JsonObject): ALManga {
|
||||||
|
val date = try {
|
||||||
|
val date = Calendar.getInstance()
|
||||||
|
date.set(
|
||||||
|
struct["startDate"]["year"].nullInt ?: 0,
|
||||||
|
(struct["startDate"]["month"].nullInt ?: 0) - 1,
|
||||||
|
struct["startDate"]["day"].nullInt ?: 0
|
||||||
|
)
|
||||||
|
date.timeInMillis
|
||||||
|
} catch (_: Exception) {
|
||||||
|
0L
|
||||||
|
}
|
||||||
|
|
||||||
|
return ALManga(
|
||||||
|
struct["id"].asInt,
|
||||||
|
struct["title"]["romaji"].asString,
|
||||||
|
struct["coverImage"]["large"].asString,
|
||||||
|
struct["description"].nullString.orEmpty(),
|
||||||
|
struct["type"].asString,
|
||||||
|
struct["status"].asString,
|
||||||
|
date,
|
||||||
|
struct["chapters"].nullInt ?: 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun jsonToALUserManga(struct: JsonObject): ALUserManga {
|
||||||
|
return ALUserManga(
|
||||||
|
struct["id"].asLong,
|
||||||
|
struct["status"].asString,
|
||||||
|
struct["scoreRaw"].asInt,
|
||||||
|
struct["progress"].asInt,
|
||||||
|
jsonToALManga(struct["media"].obj)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val clientId = "385"
|
||||||
|
private const val apiUrl = "https://graphql.anilist.co/"
|
||||||
|
private const val baseUrl = "https://anilist.co/api/v2/"
|
||||||
|
private const val baseMangaUrl = "https://anilist.co/manga/"
|
||||||
|
|
||||||
|
fun mangaUrl(mediaId: Int): String {
|
||||||
|
return baseMangaUrl + mediaId
|
||||||
|
}
|
||||||
|
|
||||||
|
fun authUrl() = Uri.parse("${baseUrl}oauth/authorize").buildUpon()
|
||||||
|
.appendQueryParameter("client_id", clientId)
|
||||||
|
.appendQueryParameter("response_type", "token")
|
||||||
|
.build()!!
|
||||||
|
|
||||||
|
fun addToLibraryQuery() = """
|
||||||
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
|
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
|
||||||
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) {
|
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) {
|
||||||
| id
|
| id
|
||||||
@ -37,36 +221,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
|}
|
|}
|
||||||
|}
|
|}
|
||||||
|""".trimMargin()
|
|""".trimMargin()
|
||||||
val variables = jsonObject(
|
|
||||||
"mangaId" to track.media_id,
|
|
||||||
"progress" to track.last_chapter_read,
|
|
||||||
"status" to track.toAnilistStatus()
|
|
||||||
)
|
|
||||||
val payload = jsonObject(
|
|
||||||
"query" to query,
|
|
||||||
"variables" to variables
|
|
||||||
)
|
|
||||||
val body = payload.toString().toRequestBody(jsonMime)
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url(apiUrl)
|
|
||||||
.post(body)
|
|
||||||
.build()
|
|
||||||
return authClient.newCall(request)
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map { netResponse ->
|
|
||||||
val responseBody = netResponse.body?.string().orEmpty()
|
|
||||||
netResponse.close()
|
|
||||||
if (responseBody.isEmpty()) {
|
|
||||||
throw Exception("Null Response")
|
|
||||||
}
|
|
||||||
val response = parser.parse(responseBody).obj
|
|
||||||
track.library_id = response["data"]["SaveMediaListEntry"]["id"].asLong
|
|
||||||
track
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateLibManga(track: Track): Observable<Track> {
|
fun updateInLibraryQuery() = """
|
||||||
val query = """
|
|
||||||
|mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) {
|
|mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) {
|
||||||
|SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) {
|
|SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) {
|
||||||
|id
|
|id
|
||||||
@ -75,30 +231,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
|}
|
|}
|
||||||
|}
|
|}
|
||||||
|""".trimMargin()
|
|""".trimMargin()
|
||||||
val variables = jsonObject(
|
|
||||||
"listId" to track.library_id,
|
|
||||||
"progress" to track.last_chapter_read,
|
|
||||||
"status" to track.toAnilistStatus(),
|
|
||||||
"score" to track.score.toInt()
|
|
||||||
)
|
|
||||||
val payload = jsonObject(
|
|
||||||
"query" to query,
|
|
||||||
"variables" to variables
|
|
||||||
)
|
|
||||||
val body = payload.toString().toRequestBody(jsonMime)
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url(apiUrl)
|
|
||||||
.post(body)
|
|
||||||
.build()
|
|
||||||
return authClient.newCall(request)
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map {
|
|
||||||
track
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun search(search: String): Observable<List<TrackSearch>> {
|
fun searchQuery() = """
|
||||||
val query = """
|
|
||||||
|query Search(${'$'}query: String) {
|
|query Search(${'$'}query: String) {
|
||||||
|Page (perPage: 50) {
|
|Page (perPage: 50) {
|
||||||
|media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
|
|media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
|
||||||
@ -122,37 +256,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
|}
|
|}
|
||||||
|}
|
|}
|
||||||
|""".trimMargin()
|
|""".trimMargin()
|
||||||
val variables = jsonObject(
|
|
||||||
"query" to search
|
|
||||||
)
|
|
||||||
val payload = jsonObject(
|
|
||||||
"query" to query,
|
|
||||||
"variables" to variables
|
|
||||||
)
|
|
||||||
val body = payload.toString().toRequestBody(jsonMime)
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url(apiUrl)
|
|
||||||
.post(body)
|
|
||||||
.build()
|
|
||||||
return authClient.newCall(request)
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map { netResponse ->
|
|
||||||
val responseBody = netResponse.body?.string().orEmpty()
|
|
||||||
if (responseBody.isEmpty()) {
|
|
||||||
throw Exception("Null Response")
|
|
||||||
}
|
|
||||||
val response = parser.parse(responseBody).obj
|
|
||||||
val data = response["data"]!!.obj
|
|
||||||
val page = data["Page"].obj
|
|
||||||
val media = page["media"].array
|
|
||||||
val entries = media.map { jsonToALManga(it.obj) }
|
|
||||||
entries.map { it.toTrack() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
fun findLibraryMangaQuery() = """
|
||||||
fun findLibManga(track: Track, userid: Int): Observable<Track?> {
|
|
||||||
val query = """
|
|
||||||
|query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
|
|query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
|
||||||
|Page {
|
|Page {
|
||||||
|mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
|
|mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
|
||||||
@ -182,47 +287,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
|}
|
|}
|
||||||
|}
|
|}
|
||||||
|""".trimMargin()
|
|""".trimMargin()
|
||||||
val variables = jsonObject(
|
|
||||||
"id" to userid,
|
|
||||||
"manga_id" to track.media_id
|
|
||||||
)
|
|
||||||
val payload = jsonObject(
|
|
||||||
"query" to query,
|
|
||||||
"variables" to variables
|
|
||||||
)
|
|
||||||
val body = payload.toString().toRequestBody(jsonMime)
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url(apiUrl)
|
|
||||||
.post(body)
|
|
||||||
.build()
|
|
||||||
return authClient.newCall(request)
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map { netResponse ->
|
|
||||||
val responseBody = netResponse.body?.string().orEmpty()
|
|
||||||
if (responseBody.isEmpty()) {
|
|
||||||
throw Exception("Null Response")
|
|
||||||
}
|
|
||||||
val response = parser.parse(responseBody).obj
|
|
||||||
val data = response["data"]!!.obj
|
|
||||||
val page = data["Page"].obj
|
|
||||||
val media = page["mediaList"].array
|
|
||||||
val entries = media.map { jsonToALUserManga(it.obj) }
|
|
||||||
entries.firstOrNull()?.toTrack()
|
|
||||||
|
|
||||||
}
|
fun currentUserQuery() = """
|
||||||
}
|
|
||||||
|
|
||||||
fun getLibManga(track: Track, userid: Int): Observable<Track> {
|
|
||||||
return findLibManga(track, userid)
|
|
||||||
.map { it ?: throw Exception("Could not find manga") }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createOAuth(token: String): OAuth {
|
|
||||||
return OAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getCurrentUser(): Observable<Pair<Int, String>> {
|
|
||||||
val query = """
|
|
||||||
|query User {
|
|query User {
|
||||||
|Viewer {
|
|Viewer {
|
||||||
|id
|
|id
|
||||||
@ -232,62 +298,5 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
|}
|
|}
|
||||||
|}
|
|}
|
||||||
|""".trimMargin()
|
|""".trimMargin()
|
||||||
val payload = jsonObject(
|
|
||||||
"query" to query
|
|
||||||
)
|
|
||||||
val body = payload.toString().toRequestBody(jsonMime)
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url(apiUrl)
|
|
||||||
.post(body)
|
|
||||||
.build()
|
|
||||||
return authClient.newCall(request)
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map { netResponse ->
|
|
||||||
val responseBody = netResponse.body?.string().orEmpty()
|
|
||||||
if (responseBody.isEmpty()) {
|
|
||||||
throw Exception("Null Response")
|
|
||||||
}
|
|
||||||
val response = parser.parse(responseBody).obj
|
|
||||||
val data = response["data"]!!.obj
|
|
||||||
val viewer = data["Viewer"].obj
|
|
||||||
Pair(viewer["id"].asInt, viewer["mediaListOptions"]["scoreFormat"].asString)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun jsonToALManga(struct: JsonObject): ALManga {
|
|
||||||
val date = try {
|
|
||||||
val date = Calendar.getInstance()
|
|
||||||
date.set(struct["startDate"]["year"].nullInt ?: 0, (struct["startDate"]["month"].nullInt ?: 0) - 1,
|
|
||||||
struct["startDate"]["day"].nullInt ?: 0)
|
|
||||||
date.timeInMillis
|
|
||||||
} catch (_: Exception) {
|
|
||||||
0L
|
|
||||||
}
|
|
||||||
|
|
||||||
return ALManga(struct["id"].asInt, struct["title"]["romaji"].asString, struct["coverImage"]["large"].asString,
|
|
||||||
struct["description"].nullString.orEmpty(), struct["type"].asString, struct["status"].asString,
|
|
||||||
date, struct["chapters"].nullInt ?: 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun jsonToALUserManga(struct: JsonObject): ALUserManga {
|
|
||||||
return ALUserManga(struct["id"].asLong, struct["status"].asString, struct["scoreRaw"].asInt, struct["progress"].asInt, jsonToALManga(struct["media"].obj))
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val clientId = "385"
|
|
||||||
private const val clientUrl = "tachiyomi://anilist-auth"
|
|
||||||
private const val apiUrl = "https://graphql.anilist.co/"
|
|
||||||
private const val baseUrl = "https://anilist.co/api/v2/"
|
|
||||||
private const val baseMangaUrl = "https://anilist.co/manga/"
|
|
||||||
|
|
||||||
fun mangaUrl(mediaId: Int): String {
|
|
||||||
return baseMangaUrl + mediaId
|
|
||||||
}
|
|
||||||
|
|
||||||
fun authUrl() = Uri.parse("${baseUrl}oauth/authorize").buildUpon()
|
|
||||||
.appendQueryParameter("client_id", clientId)
|
|
||||||
.appendQueryParameter("response_type", "token")
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import okhttp3.Interceptor
|
|||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
|
||||||
|
|
||||||
class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Interceptor {
|
class AnilistInterceptor(private val anilist: Anilist, private var token: String?) : Interceptor {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OAuth object used for authenticated requests.
|
* OAuth object used for authenticated requests.
|
||||||
|
@ -9,6 +9,15 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
|
data class OAuth(
|
||||||
|
val access_token: String,
|
||||||
|
val token_type: String,
|
||||||
|
val expires: Long,
|
||||||
|
val expires_in: Long) {
|
||||||
|
|
||||||
|
fun isExpired() = System.currentTimeMillis() > expires
|
||||||
|
}
|
||||||
|
|
||||||
data class ALManga(
|
data class ALManga(
|
||||||
val media_id: Int,
|
val media_id: Int,
|
||||||
val title_romaji: String,
|
val title_romaji: String,
|
||||||
@ -56,7 +65,7 @@ data class ALUserManga(
|
|||||||
total_chapters = manga.total_chapters
|
total_chapters = manga.total_chapters
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toTrackStatus() = when (list_status) {
|
private fun toTrackStatus() = when (list_status) {
|
||||||
"CURRENT" -> Anilist.READING
|
"CURRENT" -> Anilist.READING
|
||||||
"COMPLETED" -> Anilist.COMPLETED
|
"COMPLETED" -> Anilist.COMPLETED
|
||||||
"PAUSED" -> Anilist.PAUSED
|
"PAUSED" -> Anilist.PAUSED
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.anilist
|
|
||||||
|
|
||||||
data class OAuth(
|
|
||||||
val access_token: String,
|
|
||||||
val token_type: String,
|
|
||||||
val expires: Long,
|
|
||||||
val expires_in: Long) {
|
|
||||||
|
|
||||||
fun isExpired() = System.currentTimeMillis() > expires
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.bangumi
|
|
||||||
|
|
||||||
data class Avatar(
|
|
||||||
val large: String? = "",
|
|
||||||
val medium: String? = "",
|
|
||||||
val small: String? = ""
|
|
||||||
)
|
|
@ -7,8 +7,7 @@ import eu.kanade.tachiyomi.R
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import rx.Completable
|
import timber.log.Timber
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class Bangumi(private val context: Context, id: Int) : TrackService(id) {
|
class Bangumi(private val context: Context, id: Int) : TrackService(id) {
|
||||||
@ -29,55 +28,44 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
return track.score.toInt().toString()
|
return track.score.toInt().toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun add(track: Track): Observable<Track> {
|
override suspend fun update(track: Track): Track {
|
||||||
return api.addLibManga(track)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun update(track: Track): Observable<Track> {
|
|
||||||
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||||
track.status = COMPLETED
|
track.status = COMPLETED
|
||||||
}
|
}
|
||||||
return api.updateLibManga(track)
|
return api.updateLibManga(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun bind(track: Track): Observable<Track> {
|
override suspend fun bind(track: Track): Track {
|
||||||
return api.statusLibManga(track)
|
val statusTrack = api.statusLibManga(track)
|
||||||
.flatMap {
|
val remoteTrack = api.findLibManga(track)
|
||||||
api.findLibManga(track).flatMap { remoteTrack ->
|
if (statusTrack != null && remoteTrack != null) {
|
||||||
if (remoteTrack != null && it != null) {
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.library_id = remoteTrack.library_id
|
||||||
track.library_id = remoteTrack.library_id
|
track.status = remoteTrack.status
|
||||||
track.status = remoteTrack.status
|
track.last_chapter_read = remoteTrack.last_chapter_read
|
||||||
track.last_chapter_read = remoteTrack.last_chapter_read
|
refresh(track)
|
||||||
refresh(track)
|
} else {
|
||||||
} else {
|
track.score = DEFAULT_SCORE.toFloat()
|
||||||
// Set default fields if it's not found in the list
|
track.status = DEFAULT_STATUS
|
||||||
track.score = DEFAULT_SCORE.toFloat()
|
api.addLibManga(track)
|
||||||
track.status = DEFAULT_STATUS
|
update(track)
|
||||||
add(track)
|
}
|
||||||
update(track)
|
return track
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun search(query: String): Observable<List<TrackSearch>> {
|
override suspend fun search(query: String): List<TrackSearch> {
|
||||||
return api.search(query)
|
return api.search(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun refresh(track: Track): Observable<Track> {
|
override suspend fun refresh(track: Track): Track {
|
||||||
return api.statusLibManga(track)
|
val statusTrack = api.statusLibManga(track)
|
||||||
.flatMap {
|
track.copyPersonalFrom(statusTrack!!)
|
||||||
track.copyPersonalFrom(it!!)
|
val remoteTrack = api.findLibManga(track)
|
||||||
api.findLibManga(track)
|
if(remoteTrack != null){
|
||||||
.map { remoteTrack ->
|
track.total_chapters = remoteTrack.total_chapters
|
||||||
if (remoteTrack != null) {
|
track.status = remoteTrack.status
|
||||||
track.total_chapters = remoteTrack.total_chapters
|
}
|
||||||
track.status = remoteTrack.status
|
return track
|
||||||
}
|
|
||||||
track
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLogo() = R.drawable.tracker_bangumi
|
override fun getLogo() = R.drawable.tracker_bangumi
|
||||||
@ -99,17 +87,20 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun login(username: String, password: String) = login(password)
|
override suspend fun login(username: String, password: String): Boolean = login(password)
|
||||||
|
|
||||||
fun login(code: String): Completable {
|
suspend fun login(code: String): Boolean {
|
||||||
return api.accessToken(code).map { oauth: OAuth? ->
|
try {
|
||||||
|
|
||||||
|
val oauth = api.accessToken(code)
|
||||||
interceptor.newAuth(oauth)
|
interceptor.newAuth(oauth)
|
||||||
if (oauth != null) {
|
saveCredentials(oauth.user_id.toString(), oauth.access_token)
|
||||||
saveCredentials(oauth.user_id.toString(), oauth.access_token)
|
return true
|
||||||
}
|
} catch (e: Exception) {
|
||||||
}.doOnError {
|
Timber.e(e)
|
||||||
logout()
|
logout()
|
||||||
}.toCompletable()
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveToken(oauth: OAuth?) {
|
fun saveToken(oauth: OAuth?) {
|
||||||
@ -128,15 +119,15 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
override fun logout() {
|
override fun logout() {
|
||||||
super.logout()
|
super.logout()
|
||||||
preferences.trackToken(this).set(null)
|
preferences.trackToken(this).set(null)
|
||||||
interceptor.newAuth(null)
|
interceptor.clearOauth()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val READING = 3
|
const val PLANNING = 1
|
||||||
const val COMPLETED = 2
|
const val COMPLETED = 2
|
||||||
|
const val READING = 3
|
||||||
const val ON_HOLD = 4
|
const val ON_HOLD = 4
|
||||||
const val DROPPED = 5
|
const val DROPPED = 5
|
||||||
const val PLANNING = 1
|
|
||||||
|
|
||||||
const val DEFAULT_STATUS = READING
|
const val DEFAULT_STATUS = READING
|
||||||
const val DEFAULT_SCORE = 0
|
const val DEFAULT_SCORE = 0
|
||||||
|
@ -10,91 +10,86 @@ import eu.kanade.tachiyomi.data.database.models.Track
|
|||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.await
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.CacheControl
|
import okhttp3.CacheControl
|
||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
|
|
||||||
class BangumiApi(private val client: OkHttpClient, interceptor: BangumiInterceptor) {
|
class BangumiApi(private val client: OkHttpClient, interceptor: BangumiInterceptor) {
|
||||||
|
|
||||||
private val gson: Gson by injectLazy()
|
private val gson: Gson by injectLazy()
|
||||||
private val parser = JsonParser()
|
|
||||||
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||||
|
|
||||||
fun addLibManga(track: Track): Observable<Track> {
|
suspend fun addLibManga(track: Track): Track {
|
||||||
val body = FormBody.Builder()
|
val body = FormBody.Builder()
|
||||||
.add("rating", track.score.toInt().toString())
|
.add("rating", track.score.toInt().toString())
|
||||||
.add("status", track.toBangumiStatus())
|
.add("status", track.toBangumiStatus())
|
||||||
.build()
|
.build()
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url("$apiUrl/collection/${track.media_id}/update")
|
.url("$apiUrl/collection/${track.media_id}/update")
|
||||||
.post(body)
|
.post(body)
|
||||||
.build()
|
.build()
|
||||||
return authClient.newCall(request)
|
val response = authClient.newCall(request).await()
|
||||||
.asObservableSuccess()
|
return track
|
||||||
.map {
|
|
||||||
track
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateLibManga(track: Track): Observable<Track> {
|
suspend fun updateLibManga(track: Track): Track {
|
||||||
// chapter update
|
// chapter update
|
||||||
val body = FormBody.Builder()
|
return withContext(Dispatchers.IO) {
|
||||||
|
val body = FormBody.Builder()
|
||||||
.add("watched_eps", track.last_chapter_read.toString())
|
.add("watched_eps", track.last_chapter_read.toString())
|
||||||
.build()
|
.build()
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url("$apiUrl/subject/${track.media_id}/update/watched_eps")
|
.url("$apiUrl/subject/${track.media_id}/update/watched_eps")
|
||||||
.post(body)
|
.post(body)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
// read status update
|
// read status update
|
||||||
val sbody = FormBody.Builder()
|
val sbody = FormBody.Builder()
|
||||||
.add("status", track.toBangumiStatus())
|
.add("status", track.toBangumiStatus())
|
||||||
.build()
|
.build()
|
||||||
val srequest = Request.Builder()
|
val srequest = Request.Builder()
|
||||||
.url("$apiUrl/collection/${track.media_id}/update")
|
.url("$apiUrl/collection/${track.media_id}/update")
|
||||||
.post(sbody)
|
.post(sbody)
|
||||||
.build()
|
.build()
|
||||||
return authClient.newCall(srequest)
|
authClient.newCall(srequest).execute()
|
||||||
.asObservableSuccess()
|
authClient.newCall(request).execute()
|
||||||
.map {
|
track
|
||||||
track
|
}
|
||||||
}.flatMap {
|
|
||||||
authClient.newCall(request)
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map {
|
|
||||||
track
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun search(search: String): Observable<List<TrackSearch>> {
|
suspend fun search(search: String): List<TrackSearch> {
|
||||||
val url = Uri.parse(
|
return withContext(Dispatchers.IO) {
|
||||||
"$apiUrl/search/subject/${URLEncoder.encode(search, Charsets.UTF_8.name())}").buildUpon()
|
val url = Uri.parse(
|
||||||
|
"$apiUrl/search/subject/${URLEncoder.encode(search, Charsets.UTF_8.name())}"
|
||||||
|
).buildUpon()
|
||||||
.appendQueryParameter("max_results", "20")
|
.appendQueryParameter("max_results", "20")
|
||||||
.build()
|
.build()
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url(url.toString())
|
.url(url.toString())
|
||||||
.get()
|
.get()
|
||||||
.build()
|
.build()
|
||||||
return authClient.newCall(request)
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map { netResponse ->
|
|
||||||
var responseBody = netResponse.body?.string().orEmpty()
|
|
||||||
if (responseBody.isEmpty()) {
|
|
||||||
throw Exception("Null Response")
|
|
||||||
}
|
|
||||||
if (responseBody.contains("\"code\":404")) {
|
|
||||||
responseBody = "{\"results\":0,\"list\":[]}"
|
|
||||||
}
|
|
||||||
val response = parser.parse(responseBody).obj["list"]?.array
|
|
||||||
response?.filter { it.obj["type"].asInt == 1 }?.map { jsonToSearch(it.obj) }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
val netResponse = authClient.newCall(request).await()
|
||||||
|
var responseBody = netResponse.body?.string().orEmpty()
|
||||||
|
if (responseBody.isEmpty()) {
|
||||||
|
throw Exception("Null Response")
|
||||||
|
}
|
||||||
|
if (responseBody.contains("\"code\":404")) {
|
||||||
|
responseBody = "{\"results\":0,\"list\":[]}"
|
||||||
|
}
|
||||||
|
val response = JsonParser.parseString(responseBody).obj["list"]?.array
|
||||||
|
if (response != null) {
|
||||||
|
response.filter { it.obj["type"].asInt == 1 }?.map { jsonToSearch(it.obj) }
|
||||||
|
} else {
|
||||||
|
listOf()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun jsonToSearch(obj: JsonObject): TrackSearch {
|
private fun jsonToSearch(obj: JsonObject): TrackSearch {
|
||||||
@ -119,60 +114,56 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findLibManga(track: Track): Observable<Track?> {
|
suspend fun findLibManga(track: Track): Track? {
|
||||||
val urlMangas = "$apiUrl/subject/${track.media_id}"
|
return withContext(Dispatchers.IO) {
|
||||||
val requestMangas = Request.Builder()
|
val urlMangas = "$apiUrl/subject/${track.media_id}"
|
||||||
|
val requestMangas = Request.Builder()
|
||||||
.url(urlMangas)
|
.url(urlMangas)
|
||||||
.get()
|
.get()
|
||||||
.build()
|
.build()
|
||||||
|
val netResponse = authClient.newCall(requestMangas).execute()
|
||||||
return authClient.newCall(requestMangas)
|
val responseBody = netResponse.body?.string().orEmpty()
|
||||||
.asObservableSuccess()
|
jsonToTrack(JsonParser.parseString(responseBody).obj)
|
||||||
.map { netResponse ->
|
}
|
||||||
// get comic info
|
|
||||||
val responseBody = netResponse.body?.string().orEmpty()
|
|
||||||
jsonToTrack(parser.parse(responseBody).obj)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun statusLibManga(track: Track): Observable<Track?> {
|
suspend fun statusLibManga(track: Track): Track? {
|
||||||
val urlUserRead = "$apiUrl/collection/${track.media_id}"
|
val urlUserRead = "$apiUrl/collection/${track.media_id}"
|
||||||
val requestUserRead = Request.Builder()
|
val requestUserRead = Request.Builder()
|
||||||
.url(urlUserRead)
|
.url(urlUserRead)
|
||||||
.cacheControl(CacheControl.FORCE_NETWORK)
|
.cacheControl(CacheControl.FORCE_NETWORK)
|
||||||
.get()
|
.get()
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
// todo get user readed chapter here
|
// todo get user readed chapter here
|
||||||
return authClient.newCall(requestUserRead)
|
val response = authClient.newCall(requestUserRead).await()
|
||||||
.asObservableSuccess()
|
val resp = response.body?.toString()
|
||||||
.map { netResponse ->
|
val coll = gson.fromJson(resp, Collection::class.java)
|
||||||
val resp = netResponse.body?.string()
|
track.status = coll.status?.id!!
|
||||||
val coll = gson.fromJson(resp, Collection::class.java)
|
track.last_chapter_read = coll.ep_status!!
|
||||||
track.status = coll.status?.id!!
|
return track
|
||||||
track.last_chapter_read = coll.ep_status!!
|
|
||||||
track
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun accessToken(code: String): Observable<OAuth> {
|
suspend fun accessToken(code: String): OAuth {
|
||||||
return client.newCall(accessTokenRequest(code)).asObservableSuccess().map { netResponse ->
|
return withContext(Dispatchers.IO){
|
||||||
|
val netResponse = client.newCall(accessTokenRequest(code)).execute()
|
||||||
val responseBody = netResponse.body?.string().orEmpty()
|
val responseBody = netResponse.body?.string().orEmpty()
|
||||||
if (responseBody.isEmpty()) {
|
if(responseBody.isEmpty()){
|
||||||
throw Exception("Null Response")
|
throw Exception("Null Response")
|
||||||
}
|
}
|
||||||
gson.fromJson(responseBody, OAuth::class.java)
|
gson.fromJson(responseBody, OAuth::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun accessTokenRequest(code: String) = POST(oauthUrl,
|
private fun accessTokenRequest(code: String) = POST(
|
||||||
body = FormBody.Builder()
|
oauthUrl,
|
||||||
.add("grant_type", "authorization_code")
|
body = FormBody.Builder()
|
||||||
.add("client_id", clientId)
|
.add("grant_type", "authorization_code")
|
||||||
.add("client_secret", clientSecret)
|
.add("client_id", clientId)
|
||||||
.add("code", code)
|
.add("client_secret", clientSecret)
|
||||||
.add("redirect_uri", redirectUrl)
|
.add("code", code)
|
||||||
.build()
|
.add("redirect_uri", redirectUrl)
|
||||||
|
.build()
|
||||||
)
|
)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -192,20 +183,21 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun authUrl() =
|
fun authUrl() =
|
||||||
Uri.parse(loginUrl).buildUpon()
|
Uri.parse(loginUrl).buildUpon()
|
||||||
.appendQueryParameter("client_id", clientId)
|
.appendQueryParameter("client_id", clientId)
|
||||||
.appendQueryParameter("response_type", "code")
|
.appendQueryParameter("response_type", "code")
|
||||||
.appendQueryParameter("redirect_uri", redirectUrl)
|
.appendQueryParameter("redirect_uri", redirectUrl)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun refreshTokenRequest(token: String) = POST(oauthUrl,
|
fun refreshTokenRequest(token: String) = POST(
|
||||||
body = FormBody.Builder()
|
oauthUrl,
|
||||||
.add("grant_type", "refresh_token")
|
body = FormBody.Builder()
|
||||||
.add("client_id", clientId)
|
.add("grant_type", "refresh_token")
|
||||||
.add("client_secret", clientSecret)
|
.add("client_id", clientId)
|
||||||
.add("refresh_token", token)
|
.add("client_secret", clientSecret)
|
||||||
.add("redirect_uri", redirectUrl)
|
.add("refresh_token", token)
|
||||||
.build())
|
.add("redirect_uri", redirectUrl)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -47,8 +47,8 @@ class BangumiInterceptor(val bangumi: Bangumi, val gson: Gson) : Interceptor {
|
|||||||
return chain.proceed(authRequest)
|
return chain.proceed(authRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun newAuth(oauth: OAuth?) {
|
fun newAuth(oauth: OAuth) {
|
||||||
this.oauth = if (oauth == null) null else OAuth(
|
this.oauth = OAuth(
|
||||||
oauth.access_token,
|
oauth.access_token,
|
||||||
oauth.token_type,
|
oauth.token_type,
|
||||||
System.currentTimeMillis() / 1000,
|
System.currentTimeMillis() / 1000,
|
||||||
@ -58,4 +58,8 @@ class BangumiInterceptor(val bangumi: Bangumi, val gson: Gson) : Interceptor {
|
|||||||
|
|
||||||
bangumi.saveToken(oauth)
|
bangumi.saveToken(oauth)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearOauth(){
|
||||||
|
bangumi.saveToken(null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,3 +11,39 @@ data class Collection(
|
|||||||
val user: User? = User(),
|
val user: User? = User(),
|
||||||
val vol_status: Int? = 0
|
val vol_status: Int? = 0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class OAuth(
|
||||||
|
val access_token: String,
|
||||||
|
val token_type: String,
|
||||||
|
val created_at: Long,
|
||||||
|
val expires_in: Long,
|
||||||
|
val refresh_token: String?,
|
||||||
|
val user_id: Long?
|
||||||
|
) {
|
||||||
|
// Access token refresh before expired
|
||||||
|
fun isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Status(
|
||||||
|
val id: Int? = 0,
|
||||||
|
val name: String? = "",
|
||||||
|
val type: String? = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
data class User(
|
||||||
|
val avatar: Avatar? = Avatar(),
|
||||||
|
val id: Int? = 0,
|
||||||
|
val nickname: String? = "",
|
||||||
|
val sign: String? = "",
|
||||||
|
val url: String? = "",
|
||||||
|
val usergroup: Int? = 0,
|
||||||
|
val username: String? = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Avatar(
|
||||||
|
val large: String? = "",
|
||||||
|
val medium: String? = "",
|
||||||
|
val small: String? = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.bangumi
|
|
||||||
|
|
||||||
data class OAuth(
|
|
||||||
val access_token: String,
|
|
||||||
val token_type: String,
|
|
||||||
val created_at: Long,
|
|
||||||
val expires_in: Long,
|
|
||||||
val refresh_token: String?,
|
|
||||||
val user_id: Long?
|
|
||||||
) {
|
|
||||||
|
|
||||||
// Access token refresh before expired
|
|
||||||
fun isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.bangumi
|
|
||||||
|
|
||||||
data class Status(
|
|
||||||
val id: Int? = 0,
|
|
||||||
val name: String? = "",
|
|
||||||
val type: String? = ""
|
|
||||||
)
|
|
@ -1,11 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.bangumi
|
|
||||||
|
|
||||||
data class User(
|
|
||||||
val avatar: Avatar? = Avatar(),
|
|
||||||
val id: Int? = 0,
|
|
||||||
val nickname: String? = "",
|
|
||||||
val sign: String? = "",
|
|
||||||
val url: String? = "",
|
|
||||||
val usergroup: Int? = 0,
|
|
||||||
val username: String? = ""
|
|
||||||
)
|
|
@ -7,8 +7,7 @@ import eu.kanade.tachiyomi.R
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import rx.Completable
|
import timber.log.Timber
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
|
|
||||||
@ -70,11 +69,7 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
return df.format(track.score)
|
return df.format(track.score)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun add(track: Track): Observable<Track> {
|
override suspend fun update(track: Track): Track {
|
||||||
return api.addLibManga(track, getUserId())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun update(track: Track): Observable<Track> {
|
|
||||||
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||||
track.status = COMPLETED
|
track.status = COMPLETED
|
||||||
}
|
}
|
||||||
@ -82,41 +77,41 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
return api.updateLibManga(track)
|
return api.updateLibManga(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun bind(track: Track): Observable<Track> {
|
override suspend fun bind(track: Track): Track {
|
||||||
return api.findLibManga(track, getUserId())
|
val remoteTrack = api.findLibManga(track, getUserId())
|
||||||
.flatMap { remoteTrack ->
|
if (remoteTrack != null) {
|
||||||
if (remoteTrack != null) {
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.media_id = remoteTrack.media_id
|
||||||
track.media_id = remoteTrack.media_id
|
return update(track)
|
||||||
update(track)
|
} else {
|
||||||
} else {
|
track.score = DEFAULT_SCORE
|
||||||
track.score = DEFAULT_SCORE
|
track.status = DEFAULT_STATUS
|
||||||
track.status = DEFAULT_STATUS
|
return api.addLibManga(track, getUserId())
|
||||||
add(track)
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun search(query: String): Observable<List<TrackSearch>> {
|
override suspend fun search(query: String): List<TrackSearch> {
|
||||||
return api.search(query)
|
return api.search(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun refresh(track: Track): Observable<Track> {
|
override suspend fun refresh(track: Track): Track {
|
||||||
return api.getLibManga(track)
|
val remoteTrack = api.getLibManga(track)
|
||||||
.map { remoteTrack ->
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.total_chapters = remoteTrack.total_chapters
|
||||||
track.total_chapters = remoteTrack.total_chapters
|
return track
|
||||||
track
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun login(username: String, password: String): Completable {
|
override suspend fun login(username: String, password: String): Boolean {
|
||||||
return api.login(username, password)
|
try {
|
||||||
.doOnNext { interceptor.newAuth(it) }
|
val oauth = api.login(username, password)
|
||||||
.flatMap { api.getCurrentUser() }
|
interceptor.newAuth(oauth)
|
||||||
.doOnNext { userId -> saveCredentials(username, userId) }
|
val userId = api.getCurrentUser()
|
||||||
.doOnError { logout() }
|
saveCredentials(username, userId)
|
||||||
.toCompletable()
|
return true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun logout() {
|
override fun logout() {
|
||||||
@ -140,5 +135,4 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.kitsu
|
package eu.kanade.tachiyomi.data.track.kitsu
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.*
|
import com.github.salomonbrys.kotson.array
|
||||||
|
import com.github.salomonbrys.kotson.get
|
||||||
|
import com.github.salomonbrys.kotson.int
|
||||||
|
import com.github.salomonbrys.kotson.jsonObject
|
||||||
|
import com.github.salomonbrys.kotson.obj
|
||||||
|
import com.github.salomonbrys.kotson.string
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.google.gson.JsonObject
|
import com.google.gson.JsonObject
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
@ -9,240 +14,228 @@ import eu.kanade.tachiyomi.network.POST
|
|||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory
|
|
||||||
import retrofit2.converter.gson.GsonConverterFactory
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
import retrofit2.http.*
|
import retrofit2.http.Body
|
||||||
import rx.Observable
|
import retrofit2.http.Field
|
||||||
|
import retrofit2.http.FormUrlEncoded
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.Header
|
||||||
|
import retrofit2.http.Headers
|
||||||
|
import retrofit2.http.PATCH
|
||||||
|
import retrofit2.http.POST
|
||||||
|
import retrofit2.http.Path
|
||||||
|
import retrofit2.http.Query
|
||||||
|
|
||||||
class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) {
|
class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) {
|
||||||
|
|
||||||
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||||
|
|
||||||
private val rest = Retrofit.Builder()
|
private val rest = Retrofit.Builder()
|
||||||
.baseUrl(baseUrl)
|
.baseUrl(baseUrl)
|
||||||
.client(authClient)
|
.client(authClient)
|
||||||
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().serializeNulls().create()))
|
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().serializeNulls().create()))
|
||||||
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
.build()
|
||||||
.build()
|
.create(KitsuApi.Rest::class.java)
|
||||||
.create(KitsuApi.Rest::class.java)
|
|
||||||
|
|
||||||
private val searchRest = Retrofit.Builder()
|
private val searchRest = Retrofit.Builder()
|
||||||
.baseUrl(algoliaKeyUrl)
|
.baseUrl(algoliaKeyUrl)
|
||||||
.client(authClient)
|
.client(authClient)
|
||||||
.addConverterFactory(GsonConverterFactory.create())
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
.build()
|
||||||
.build()
|
.create(KitsuApi.SearchKeyRest::class.java)
|
||||||
.create(KitsuApi.SearchKeyRest::class.java)
|
|
||||||
|
|
||||||
private val algoliaRest = Retrofit.Builder()
|
private val algoliaRest = Retrofit.Builder()
|
||||||
.baseUrl(algoliaUrl)
|
.baseUrl(algoliaUrl)
|
||||||
|
.client(client)
|
||||||
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
|
.build()
|
||||||
|
.create(KitsuApi.AgoliaSearchRest::class.java)
|
||||||
|
|
||||||
|
suspend fun addLibManga(track: Track, userId: String): Track {
|
||||||
|
// @formatter:off
|
||||||
|
val data = jsonObject(
|
||||||
|
"type" to "libraryEntries",
|
||||||
|
"attributes" to jsonObject(
|
||||||
|
"status" to track.toKitsuStatus(),
|
||||||
|
"progress" to track.last_chapter_read
|
||||||
|
),
|
||||||
|
"relationships" to jsonObject(
|
||||||
|
"user" to jsonObject(
|
||||||
|
"data" to jsonObject(
|
||||||
|
"id" to userId,
|
||||||
|
"type" to "users"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"media" to jsonObject(
|
||||||
|
"data" to jsonObject(
|
||||||
|
"id" to track.media_id,
|
||||||
|
"type" to "manga"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val json = rest.addLibManga(jsonObject("data" to data))
|
||||||
|
track.media_id = json["data"]["id"].int
|
||||||
|
return track
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateLibManga(track: Track): Track {
|
||||||
|
// @formatter:off
|
||||||
|
val data = jsonObject(
|
||||||
|
"type" to "libraryEntries",
|
||||||
|
"id" to track.media_id,
|
||||||
|
"attributes" to jsonObject(
|
||||||
|
"status" to track.toKitsuStatus(),
|
||||||
|
"progress" to track.last_chapter_read,
|
||||||
|
"ratingTwenty" to track.toKitsuScore()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
// @formatter:on
|
||||||
|
|
||||||
|
rest.updateLibManga(track.media_id, jsonObject("data" to data))
|
||||||
|
return track
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun search(query: String): List<TrackSearch> {
|
||||||
|
val key = searchRest.getKey()["media"].asJsonObject["key"].string
|
||||||
|
return algoliaSearch(key, query)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun algoliaSearch(key: String, query: String): List<TrackSearch> {
|
||||||
|
val jsonObject = jsonObject("params" to "query=$query$algoliaFilter")
|
||||||
|
val json = algoliaRest.getSearchQuery(algoliaAppId, key, jsonObject)
|
||||||
|
val data = json["hits"].array
|
||||||
|
return data.map { KitsuSearchManga(it.obj) }
|
||||||
|
.filter { it.subType != "novel" }
|
||||||
|
.map { it.toTrack() }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun findLibManga(track: Track, userId: String): Track? {
|
||||||
|
val json = rest.findLibManga(track.media_id, userId)
|
||||||
|
val data = json["data"].array
|
||||||
|
return if (data.size() > 0) {
|
||||||
|
val manga = json["included"].array[0].obj
|
||||||
|
KitsuLibManga(data[0].obj, manga).toTrack()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getLibManga(track: Track): Track {
|
||||||
|
val json = rest.getLibManga(track.media_id)
|
||||||
|
val data = json["data"].array
|
||||||
|
if (data.size() > 0) {
|
||||||
|
val manga = json["included"].array[0].obj
|
||||||
|
return KitsuLibManga(data[0].obj, manga).toTrack()
|
||||||
|
} else {
|
||||||
|
throw Exception("Could not find manga")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun login(username: String, password: String): OAuth {
|
||||||
|
return Retrofit.Builder()
|
||||||
|
.baseUrl(loginUrl)
|
||||||
.client(client)
|
.client(client)
|
||||||
.addConverterFactory(GsonConverterFactory.create())
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
|
||||||
.build()
|
.build()
|
||||||
.create(KitsuApi.AgoliaSearchRest::class.java)
|
.create(KitsuApi.LoginRest::class.java)
|
||||||
|
.requestAccessToken(username, password)
|
||||||
fun addLibManga(track: Track, userId: String): Observable<Track> {
|
|
||||||
return Observable.defer {
|
|
||||||
// @formatter:off
|
|
||||||
val data = jsonObject(
|
|
||||||
"type" to "libraryEntries",
|
|
||||||
"attributes" to jsonObject(
|
|
||||||
"status" to track.toKitsuStatus(),
|
|
||||||
"progress" to track.last_chapter_read
|
|
||||||
),
|
|
||||||
"relationships" to jsonObject(
|
|
||||||
"user" to jsonObject(
|
|
||||||
"data" to jsonObject(
|
|
||||||
"id" to userId,
|
|
||||||
"type" to "users"
|
|
||||||
)
|
|
||||||
),
|
|
||||||
"media" to jsonObject(
|
|
||||||
"data" to jsonObject(
|
|
||||||
"id" to track.media_id,
|
|
||||||
"type" to "manga"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
rest.addLibManga(jsonObject("data" to data))
|
|
||||||
.map { json ->
|
|
||||||
track.media_id = json["data"]["id"].int
|
|
||||||
track
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateLibManga(track: Track): Observable<Track> {
|
suspend fun getCurrentUser(): String {
|
||||||
return Observable.defer {
|
val currentUser = rest.getCurrentUser()
|
||||||
// @formatter:off
|
return currentUser["data"].array[0]["id"].string
|
||||||
val data = jsonObject(
|
|
||||||
"type" to "libraryEntries",
|
|
||||||
"id" to track.media_id,
|
|
||||||
"attributes" to jsonObject(
|
|
||||||
"status" to track.toKitsuStatus(),
|
|
||||||
"progress" to track.last_chapter_read,
|
|
||||||
"ratingTwenty" to track.toKitsuScore()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
// @formatter:on
|
|
||||||
|
|
||||||
rest.updateLibManga(track.media_id, jsonObject("data" to data))
|
|
||||||
.map { track }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun search(query: String): Observable<List<TrackSearch>> {
|
|
||||||
return searchRest
|
|
||||||
.getKey().map { json ->
|
|
||||||
json["media"].asJsonObject["key"].string
|
|
||||||
}.flatMap { key ->
|
|
||||||
algoliaSearch(key, query)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun algoliaSearch(key: String, query: String): Observable<List<TrackSearch>> {
|
|
||||||
val jsonObject = jsonObject("params" to "query=$query$algoliaFilter")
|
|
||||||
return algoliaRest
|
|
||||||
.getSearchQuery(algoliaAppId, key, jsonObject)
|
|
||||||
.map { json ->
|
|
||||||
val data = json["hits"].array
|
|
||||||
data.map { KitsuSearchManga(it.obj) }
|
|
||||||
.filter { it.subType != "novel" }
|
|
||||||
.map { it.toTrack() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun findLibManga(track: Track, userId: String): Observable<Track?> {
|
|
||||||
return rest.findLibManga(track.media_id, userId)
|
|
||||||
.map { json ->
|
|
||||||
val data = json["data"].array
|
|
||||||
if (data.size() > 0) {
|
|
||||||
val manga = json["included"].array[0].obj
|
|
||||||
KitsuLibManga(data[0].obj, manga).toTrack()
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getLibManga(track: Track): Observable<Track> {
|
|
||||||
return rest.getLibManga(track.media_id)
|
|
||||||
.map { json ->
|
|
||||||
val data = json["data"].array
|
|
||||||
if (data.size() > 0) {
|
|
||||||
val manga = json["included"].array[0].obj
|
|
||||||
KitsuLibManga(data[0].obj, manga).toTrack()
|
|
||||||
} else {
|
|
||||||
throw Exception("Could not find manga")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun login(username: String, password: String): Observable<OAuth> {
|
|
||||||
return Retrofit.Builder()
|
|
||||||
.baseUrl(loginUrl)
|
|
||||||
.client(client)
|
|
||||||
.addConverterFactory(GsonConverterFactory.create())
|
|
||||||
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
|
||||||
.build()
|
|
||||||
.create(KitsuApi.LoginRest::class.java)
|
|
||||||
.requestAccessToken(username, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getCurrentUser(): Observable<String> {
|
|
||||||
return rest.getCurrentUser().map { it["data"].array[0]["id"].string }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private interface Rest {
|
private interface Rest {
|
||||||
|
|
||||||
@Headers("Content-Type: application/vnd.api+json")
|
@Headers("Content-Type: application/vnd.api+json")
|
||||||
@POST("library-entries")
|
@POST("library-entries")
|
||||||
fun addLibManga(
|
suspend fun addLibManga(
|
||||||
@Body data: JsonObject
|
@Body data: JsonObject
|
||||||
): Observable<JsonObject>
|
): JsonObject
|
||||||
|
|
||||||
@Headers("Content-Type: application/vnd.api+json")
|
@Headers("Content-Type: application/vnd.api+json")
|
||||||
@PATCH("library-entries/{id}")
|
@PATCH("library-entries/{id}")
|
||||||
fun updateLibManga(
|
suspend fun updateLibManga(
|
||||||
@Path("id") remoteId: Int,
|
@Path("id") remoteId: Int,
|
||||||
@Body data: JsonObject
|
@Body data: JsonObject
|
||||||
): Observable<JsonObject>
|
): JsonObject
|
||||||
|
|
||||||
|
|
||||||
@GET("library-entries")
|
@GET("library-entries")
|
||||||
fun findLibManga(
|
suspend fun findLibManga(
|
||||||
@Query("filter[manga_id]", encoded = true) remoteId: Int,
|
@Query("filter[manga_id]", encoded = true) remoteId: Int,
|
||||||
@Query("filter[user_id]", encoded = true) userId: String,
|
@Query("filter[user_id]", encoded = true) userId: String,
|
||||||
@Query("include") includes: String = "manga"
|
@Query("include") includes: String = "manga"
|
||||||
): Observable<JsonObject>
|
): JsonObject
|
||||||
|
|
||||||
@GET("library-entries")
|
@GET("library-entries")
|
||||||
fun getLibManga(
|
suspend fun getLibManga(
|
||||||
@Query("filter[id]", encoded = true) remoteId: Int,
|
@Query("filter[id]", encoded = true) remoteId: Int,
|
||||||
@Query("include") includes: String = "manga"
|
@Query("include") includes: String = "manga"
|
||||||
): Observable<JsonObject>
|
): JsonObject
|
||||||
|
|
||||||
@GET("users")
|
@GET("users")
|
||||||
fun getCurrentUser(
|
suspend fun getCurrentUser(
|
||||||
@Query("filter[self]", encoded = true) self: Boolean = true
|
@Query("filter[self]", encoded = true) self: Boolean = true
|
||||||
): Observable<JsonObject>
|
): JsonObject
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private interface SearchKeyRest {
|
private interface SearchKeyRest {
|
||||||
@GET("media/")
|
@GET("media/")
|
||||||
fun getKey(): Observable<JsonObject>
|
suspend fun getKey(): JsonObject
|
||||||
}
|
}
|
||||||
|
|
||||||
private interface AgoliaSearchRest {
|
private interface AgoliaSearchRest {
|
||||||
@POST("query/")
|
@POST("query/")
|
||||||
fun getSearchQuery(@Header("X-Algolia-Application-Id") appid: String, @Header("X-Algolia-API-Key") key: String, @Body json: JsonObject): Observable<JsonObject>
|
suspend fun getSearchQuery(
|
||||||
|
@Header("X-Algolia-Application-Id") appid: String,
|
||||||
|
@Header("X-Algolia-API-Key") key: String,
|
||||||
|
@Body json: JsonObject
|
||||||
|
): JsonObject
|
||||||
}
|
}
|
||||||
|
|
||||||
private interface LoginRest {
|
private interface LoginRest {
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@POST("oauth/token")
|
@POST("oauth/token")
|
||||||
fun requestAccessToken(
|
suspend fun requestAccessToken(
|
||||||
@Field("username") username: String,
|
@Field("username") username: String,
|
||||||
@Field("password") password: String,
|
@Field("password") password: String,
|
||||||
@Field("grant_type") grantType: String = "password",
|
@Field("grant_type") grantType: String = "password",
|
||||||
@Field("client_id") client_id: String = clientId,
|
@Field("client_id") client_id: String = clientId,
|
||||||
@Field("client_secret") client_secret: String = clientSecret
|
@Field("client_secret") client_secret: String = clientSecret
|
||||||
): Observable<OAuth>
|
): OAuth
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val clientId = "dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd"
|
private const val clientId =
|
||||||
private const val clientSecret = "54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151"
|
"dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd"
|
||||||
|
private const val clientSecret =
|
||||||
|
"54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151"
|
||||||
private const val baseUrl = "https://kitsu.io/api/edge/"
|
private const val baseUrl = "https://kitsu.io/api/edge/"
|
||||||
private const val loginUrl = "https://kitsu.io/api/"
|
private const val loginUrl = "https://kitsu.io/api/"
|
||||||
private const val baseMangaUrl = "https://kitsu.io/manga/"
|
private const val baseMangaUrl = "https://kitsu.io/manga/"
|
||||||
private const val algoliaKeyUrl = "https://kitsu.io/api/edge/algolia-keys/"
|
private const val algoliaKeyUrl = "https://kitsu.io/api/edge/algolia-keys/"
|
||||||
private const val algoliaUrl = "https://AWQO5J657S-dsn.algolia.net/1/indexes/production_media/"
|
private const val algoliaUrl =
|
||||||
|
"https://AWQO5J657S-dsn.algolia.net/1/indexes/production_media/"
|
||||||
private const val algoliaAppId = "AWQO5J657S"
|
private const val algoliaAppId = "AWQO5J657S"
|
||||||
private const val algoliaFilter = "&facetFilters=%5B%22kind%3Amanga%22%5D&attributesToRetrieve=%5B%22synopsis%22%2C%22canonicalTitle%22%2C%22chapterCount%22%2C%22posterImage%22%2C%22startDate%22%2C%22subtype%22%2C%22endDate%22%2C%20%22id%22%5D"
|
private const val algoliaFilter =
|
||||||
|
"&facetFilters=%5B%22kind%3Amanga%22%5D&attributesToRetrieve=%5B%22synopsis%22%2C%22canonicalTitle%22%2C%22chapterCount%22%2C%22posterImage%22%2C%22startDate%22%2C%22subtype%22%2C%22endDate%22%2C%20%22id%22%5D"
|
||||||
|
|
||||||
fun mangaUrl(remoteId: Int): String {
|
fun mangaUrl(remoteId: Int): String {
|
||||||
return baseMangaUrl + remoteId
|
return baseMangaUrl + remoteId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun refreshTokenRequest(token: String) = POST(
|
||||||
fun refreshTokenRequest(token: String) = POST("${loginUrl}oauth/token",
|
"${loginUrl}oauth/token",
|
||||||
body = FormBody.Builder()
|
body = FormBody.Builder()
|
||||||
.add("grant_type", "refresh_token")
|
.add("grant_type", "refresh_token")
|
||||||
.add("client_id", clientId)
|
.add("client_id", clientId)
|
||||||
.add("client_secret", clientSecret)
|
.add("client_secret", clientSecret)
|
||||||
.add("refresh_token", token)
|
.add("refresh_token", token)
|
||||||
.build())
|
.build()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -7,33 +7,15 @@ import eu.kanade.tachiyomi.data.database.models.Track
|
|||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import okhttp3.HttpUrl
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
import rx.Completable
|
import timber.log.Timber
|
||||||
import rx.Observable
|
|
||||||
|
|
||||||
class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
|
class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val READING = 1
|
|
||||||
const val COMPLETED = 2
|
|
||||||
const val ON_HOLD = 3
|
|
||||||
const val DROPPED = 4
|
|
||||||
const val PLAN_TO_READ = 6
|
|
||||||
|
|
||||||
const val DEFAULT_STATUS = READING
|
|
||||||
const val DEFAULT_SCORE = 0
|
|
||||||
|
|
||||||
const val BASE_URL = "https://myanimelist.net"
|
|
||||||
const val USER_SESSION_COOKIE = "MALSESSIONID"
|
|
||||||
const val LOGGED_IN_COOKIE = "is_logged_in"
|
|
||||||
}
|
|
||||||
|
|
||||||
private val interceptor by lazy { MyAnimeListInterceptor(this) }
|
private val interceptor by lazy { MyAnimeListInterceptor(this) }
|
||||||
private val api by lazy { MyAnimeListApi(client, interceptor) }
|
private val api by lazy { MyAnimeListApi(client, interceptor) }
|
||||||
|
|
||||||
override val name: String
|
override val name = "MyAnimeList"
|
||||||
get() = "MyAnimeList"
|
|
||||||
|
|
||||||
override fun getLogo() = R.drawable.tracker_mal
|
override fun getLogo() = R.drawable.tracker_mal
|
||||||
|
|
||||||
@ -62,11 +44,7 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
return track.score.toInt().toString()
|
return track.score.toInt().toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun add(track: Track): Observable<Track> {
|
override suspend fun update(track: Track): Track {
|
||||||
return api.addLibManga(track)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun update(track: Track): Observable<Track> {
|
|
||||||
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||||
track.status = COMPLETED
|
track.status = COMPLETED
|
||||||
}
|
}
|
||||||
@ -74,45 +52,46 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
return api.updateLibManga(track)
|
return api.updateLibManga(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun bind(track: Track): Observable<Track> {
|
override suspend fun bind(track: Track): Track {
|
||||||
return api.findLibManga(track)
|
val remoteTrack = api.findLibManga(track)
|
||||||
.flatMap { remoteTrack ->
|
if (remoteTrack != null) {
|
||||||
if (remoteTrack != null) {
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.copyPersonalFrom(remoteTrack)
|
update(track)
|
||||||
update(track)
|
} else {
|
||||||
} else {
|
// Set default fields if it's not found in the list
|
||||||
// Set default fields if it's not found in the list
|
track.score = DEFAULT_SCORE.toFloat()
|
||||||
track.score = DEFAULT_SCORE.toFloat()
|
track.status = DEFAULT_STATUS
|
||||||
track.status = DEFAULT_STATUS
|
return api.addLibManga(track)
|
||||||
add(track)
|
}
|
||||||
}
|
return track
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun search(query: String): Observable<List<TrackSearch>> {
|
override suspend fun search(query: String): List<TrackSearch> {
|
||||||
return api.search(query)
|
return api.search(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun refresh(track: Track): Observable<Track> {
|
override suspend fun refresh(track: Track): Track {
|
||||||
return api.getLibManga(track)
|
val remoteTrack = api.getLibManga(track)
|
||||||
.map { remoteTrack ->
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.total_chapters = remoteTrack.total_chapters
|
||||||
track.total_chapters = remoteTrack.total_chapters
|
return track
|
||||||
track
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun login(username: String, password: String): Completable {
|
override suspend fun login(username: String, password: String): Boolean {
|
||||||
logout()
|
logout()
|
||||||
|
return try {
|
||||||
return Observable.fromCallable { api.login(username, password) }
|
val csrf = api.login(username, password)
|
||||||
.doOnNext { csrf -> saveCSRF(csrf) }
|
saveCSRF(csrf)
|
||||||
.doOnNext { saveCredentials(username, password) }
|
saveCredentials(username, password)
|
||||||
.doOnError { logout() }
|
true
|
||||||
.toCompletable()
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
logout()
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun refreshLogin() {
|
private suspend fun refreshLogin() {
|
||||||
val username = getUsername()
|
val username = getUsername()
|
||||||
val password = getPassword()
|
val password = getPassword()
|
||||||
logout()
|
logout()
|
||||||
@ -122,13 +101,14 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
saveCSRF(csrf)
|
saveCSRF(csrf)
|
||||||
saveCredentials(username, password)
|
saveCredentials(username, password)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
logout()
|
logout()
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to login again if cookies have been cleared but credentials are still filled
|
// Attempt to login again if cookies have been cleared but credentials are still filled
|
||||||
fun ensureLoggedIn() {
|
suspend fun ensureLoggedIn() {
|
||||||
if (isAuthorized) return
|
if (isAuthorized) return
|
||||||
if (!isLogged) throw Exception("MAL Login Credentials not found")
|
if (!isLogged) throw Exception("MAL Login Credentials not found")
|
||||||
|
|
||||||
@ -141,10 +121,7 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
networkService.cookieManager.remove(BASE_URL.toHttpUrlOrNull()!!)
|
networkService.cookieManager.remove(BASE_URL.toHttpUrlOrNull()!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
val isAuthorized: Boolean
|
private val isAuthorized = super.isLogged && getCSRF().isNotEmpty() && checkCookies()
|
||||||
get() = super.isLogged &&
|
|
||||||
getCSRF().isNotEmpty() &&
|
|
||||||
checkCookies()
|
|
||||||
|
|
||||||
fun getCSRF(): String = preferences.trackToken(this).getOrDefault()
|
fun getCSRF(): String = preferences.trackToken(this).getOrDefault()
|
||||||
|
|
||||||
@ -161,4 +138,18 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
return ckCount == 2
|
return ckCount == 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val READING = 1
|
||||||
|
const val COMPLETED = 2
|
||||||
|
const val ON_HOLD = 3
|
||||||
|
const val DROPPED = 4
|
||||||
|
const val PLAN_TO_READ = 6
|
||||||
|
|
||||||
|
const val DEFAULT_STATUS = READING
|
||||||
|
const val DEFAULT_SCORE = 0
|
||||||
|
|
||||||
|
const val BASE_URL = "https://myanimelist.net"
|
||||||
|
const val USER_SESSION_COOKIE = "MALSESSIONID"
|
||||||
|
const val LOGGED_IN_COOKIE = "is_logged_in"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,50 +6,41 @@ import eu.kanade.tachiyomi.data.track.TrackManager
|
|||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.network.asObservable
|
import eu.kanade.tachiyomi.network.await
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.consumeBody
|
||||||
|
import eu.kanade.tachiyomi.network.consumeXmlBody
|
||||||
import eu.kanade.tachiyomi.util.selectInt
|
import eu.kanade.tachiyomi.util.selectInt
|
||||||
import eu.kanade.tachiyomi.util.selectText
|
import eu.kanade.tachiyomi.util.selectText
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import okhttp3.Response
|
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import org.jsoup.parser.Parser
|
import org.jsoup.parser.Parser
|
||||||
import rx.Observable
|
|
||||||
import java.io.BufferedReader
|
|
||||||
import java.io.InputStreamReader
|
|
||||||
import java.util.zip.GZIPInputStream
|
|
||||||
|
|
||||||
|
|
||||||
class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListInterceptor) {
|
class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListInterceptor) {
|
||||||
|
|
||||||
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||||
|
|
||||||
fun search(query: String): Observable<List<TrackSearch>> {
|
suspend fun search(query: String): List<TrackSearch> {
|
||||||
return if (query.startsWith(PREFIX_MY)) {
|
return withContext(Dispatchers.IO) {
|
||||||
val realQuery = query.removePrefix(PREFIX_MY)
|
if (query.startsWith(PREFIX_MY)) {
|
||||||
getList()
|
queryUsersList(query)
|
||||||
.flatMap { Observable.from(it) }
|
} else {
|
||||||
.filter { it.title.contains(realQuery, true) }
|
val realQuery = query.take(100)
|
||||||
.toList()
|
val response = client.newCall(GET(searchUrl(realQuery))).await()
|
||||||
} else {
|
val matches = Jsoup.parse(response.consumeBody())
|
||||||
client.newCall(GET(searchUrl(query)))
|
.select("div.js-categories-seasonal.js-block-list.list")
|
||||||
.asObservable()
|
.select("table").select("tbody")
|
||||||
.flatMap { response ->
|
.select("tr").drop(1)
|
||||||
Observable.from(Jsoup.parse(response.consumeBody())
|
|
||||||
.select("div.js-categories-seasonal.js-block-list.list")
|
matches.filter { row -> row.select(TD)[2].text() != "Novel" }
|
||||||
.select("table").select("tbody")
|
|
||||||
.select("tr").drop(1))
|
|
||||||
}
|
|
||||||
.filter { row ->
|
|
||||||
row.select(TD)[2].text() != "Novel"
|
|
||||||
}
|
|
||||||
.map { row ->
|
.map { row ->
|
||||||
TrackSearch.create(TrackManager.MYANIMELIST).apply {
|
TrackSearch.create(TrackManager.MYANIMELIST).apply {
|
||||||
title = row.searchTitle()
|
title = row.searchTitle()
|
||||||
@ -64,136 +55,119 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.toList()
|
.toList()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addLibManga(track: Track): Observable<Track> {
|
private suspend fun queryUsersList(query: String): List<TrackSearch> {
|
||||||
return Observable.defer {
|
val realQuery = query.removePrefix(PREFIX_MY).take(100)
|
||||||
authClient.newCall(POST(url = addUrl(), body = mangaPostPayload(track)))
|
return getList().filter { it.title.contains(realQuery, true) }.toList()
|
||||||
.asObservableSuccess()
|
|
||||||
.map { track }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateLibManga(track: Track): Observable<Track> {
|
suspend fun addLibManga(track: Track): Track {
|
||||||
return Observable.defer {
|
authClient.newCall(POST(url = addUrl(), body = mangaPostPayload(track))).await()
|
||||||
authClient.newCall(POST(url = updateUrl(), body = mangaPostPayload(track)))
|
return track
|
||||||
.asObservableSuccess()
|
|
||||||
.map { track }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findLibManga(track: Track): Observable<Track?> {
|
suspend fun updateLibManga(track: Track): Track {
|
||||||
return authClient.newCall(GET(url = listEntryUrl(track.media_id)))
|
authClient.newCall(POST(url = updateUrl(), body = mangaPostPayload(track))).await()
|
||||||
.asObservable()
|
return track
|
||||||
.map {response ->
|
}
|
||||||
var libTrack: Track? = null
|
|
||||||
response.use {
|
|
||||||
if (it.priorResponse?.isRedirect != true) {
|
|
||||||
val trackForm = Jsoup.parse(it.consumeBody())
|
|
||||||
|
|
||||||
libTrack = Track.create(TrackManager.MYANIMELIST).apply {
|
suspend fun findLibManga(track: Track): Track? {
|
||||||
last_chapter_read = trackForm.select("#add_manga_num_read_chapters").`val`().toInt()
|
return withContext(Dispatchers.IO) {
|
||||||
total_chapters = trackForm.select("#totalChap").text().toInt()
|
val response = authClient.newCall(GET(url = listEntryUrl(track.media_id))).await()
|
||||||
status = trackForm.select("#add_manga_status > option[selected]").`val`().toInt()
|
var remoteTrack: Track? = null
|
||||||
score = trackForm.select("#add_manga_score > option[selected]").`val`().toFloatOrNull() ?: 0f
|
response.use {
|
||||||
}
|
if (it.priorResponse?.isRedirect != true) {
|
||||||
}
|
val trackForm = Jsoup.parse(it.consumeBody())
|
||||||
|
|
||||||
|
remoteTrack = Track.create(TrackManager.MYANIMELIST).apply {
|
||||||
|
last_chapter_read =
|
||||||
|
trackForm.select("#add_manga_num_read_chapters").`val`().toInt()
|
||||||
|
total_chapters = trackForm.select("#totalChap").text().toInt()
|
||||||
|
status =
|
||||||
|
trackForm.select("#add_manga_status > option[selected]").`val`().toInt()
|
||||||
|
score = trackForm.select("#add_manga_score > option[selected]").`val`()
|
||||||
|
.toFloatOrNull() ?: 0f
|
||||||
}
|
}
|
||||||
libTrack
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
remoteTrack
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLibManga(track: Track): Observable<Track> {
|
suspend fun getLibManga(track: Track): Track {
|
||||||
return findLibManga(track)
|
val result = findLibManga(track)
|
||||||
.map { it ?: throw Exception("Could not find manga") }
|
if (result == null) {
|
||||||
|
throw Exception("Could not find manga")
|
||||||
|
} else {
|
||||||
|
return result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun login(username: String, password: String): String {
|
suspend fun login(username: String, password: String): String {
|
||||||
val csrf = getSessionInfo()
|
return withContext(Dispatchers.IO) {
|
||||||
|
val csrf = getSessionInfo()
|
||||||
login(username, password, csrf)
|
login(username, password, csrf)
|
||||||
|
csrf
|
||||||
return csrf
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getSessionInfo(): String {
|
private suspend fun getSessionInfo(): String {
|
||||||
val response = client.newCall(GET(loginUrl())).execute()
|
val response = client.newCall(GET(loginUrl())).execute()
|
||||||
|
|
||||||
return Jsoup.parse(response.consumeBody())
|
return Jsoup.parse(response.consumeBody())
|
||||||
.select("meta[name=csrf_token]")
|
.select("meta[name=csrf_token]")
|
||||||
.attr("content")
|
.attr("content")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun login(username: String, password: String, csrf: String) {
|
private suspend fun login(username: String, password: String, csrf: String) {
|
||||||
val response = client.newCall(POST(url = loginUrl(), body = loginPostBody(username, password, csrf))).execute()
|
withContext(Dispatchers.IO) {
|
||||||
|
val response =
|
||||||
|
client.newCall(POST(loginUrl(), body = loginPostBody(username, password, csrf)))
|
||||||
|
.execute()
|
||||||
|
|
||||||
response.use {
|
response.use {
|
||||||
if (response.priorResponse?.code != 302) throw Exception("Authentication error")
|
if (response.priorResponse?.code != 302) throw Exception("Authentication error")
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getList(): Observable<List<TrackSearch>> {
|
|
||||||
return getListUrl()
|
|
||||||
.flatMap { url ->
|
|
||||||
getListXml(url)
|
|
||||||
}
|
|
||||||
.flatMap { doc ->
|
|
||||||
Observable.from(doc.select("manga"))
|
|
||||||
}
|
|
||||||
.map {
|
|
||||||
TrackSearch.create(TrackManager.MYANIMELIST).apply {
|
|
||||||
title = it.selectText("manga_title")!!
|
|
||||||
media_id = it.selectInt("manga_mangadb_id")
|
|
||||||
last_chapter_read = it.selectInt("my_read_chapters")
|
|
||||||
status = getStatus(it.selectText("my_status")!!)
|
|
||||||
score = it.selectInt("my_score").toFloat()
|
|
||||||
total_chapters = it.selectInt("manga_chapters")
|
|
||||||
tracking_url = mangaUrl(media_id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getListUrl(): Observable<String> {
|
|
||||||
return authClient.newCall(POST(url = exportListUrl(), body = exportPostBody()))
|
|
||||||
.asObservable()
|
|
||||||
.map {response ->
|
|
||||||
baseUrl + Jsoup.parse(response.consumeBody())
|
|
||||||
.select("div.goodresult")
|
|
||||||
.select("a")
|
|
||||||
.attr("href")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getListXml(url: String): Observable<Document> {
|
|
||||||
return authClient.newCall(GET(url))
|
|
||||||
.asObservable()
|
|
||||||
.map { response ->
|
|
||||||
Jsoup.parse(response.consumeXmlBody(), "", Parser.xmlParser())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Response.consumeBody(): String? {
|
|
||||||
use {
|
|
||||||
if (it.code != 200) throw Exception("HTTP error ${it.code}")
|
|
||||||
return it.body?.string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Response.consumeXmlBody(): String? {
|
|
||||||
use { res ->
|
|
||||||
if (res.code != 200) throw Exception("Export list error")
|
|
||||||
BufferedReader(InputStreamReader(GZIPInputStream(res.body?.source()?.inputStream()))).use { reader ->
|
|
||||||
val sb = StringBuilder()
|
|
||||||
reader.forEachLine { line ->
|
|
||||||
sb.append(line)
|
|
||||||
}
|
|
||||||
return sb.toString()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun getList(): List<TrackSearch> {
|
||||||
|
val results = getListXml(getListUrl()).select("manga")
|
||||||
|
|
||||||
|
return results.map {
|
||||||
|
TrackSearch.create(TrackManager.MYANIMELIST).apply {
|
||||||
|
title = it.selectText("manga_title")!!
|
||||||
|
media_id = it.selectInt("manga_mangadb_id")
|
||||||
|
last_chapter_read = it.selectInt("my_read_chapters")
|
||||||
|
status = getStatus(it.selectText("my_status")!!)
|
||||||
|
score = it.selectInt("my_score").toFloat()
|
||||||
|
total_chapters = it.selectInt("manga_chapters")
|
||||||
|
tracking_url = mangaUrl(media_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getListUrl(): String {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
val response =
|
||||||
|
authClient.newCall(POST(url = exportListUrl(), body = exportPostBody())).execute()
|
||||||
|
|
||||||
|
baseUrl + Jsoup.parse(response.consumeBody())
|
||||||
|
.select("div.goodresult")
|
||||||
|
.select("a")
|
||||||
|
.attr("href")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getListXml(url: String): Document {
|
||||||
|
val response = authClient.newCall(GET(url)).await()
|
||||||
|
return Jsoup.parse(response.consumeXmlBody(), "", Parser.xmlParser())
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val CSRF = "csrf_token"
|
const val CSRF = "csrf_token"
|
||||||
|
|
||||||
@ -206,88 +180,91 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
|||||||
private fun mangaUrl(remoteId: Int) = baseMangaUrl + remoteId
|
private fun mangaUrl(remoteId: Int) = baseMangaUrl + remoteId
|
||||||
|
|
||||||
private fun loginUrl() = Uri.parse(baseUrl).buildUpon()
|
private fun loginUrl() = Uri.parse(baseUrl).buildUpon()
|
||||||
.appendPath("login.php")
|
.appendPath("login.php")
|
||||||
.toString()
|
.toString()
|
||||||
|
|
||||||
private fun searchUrl(query: String): String {
|
private fun searchUrl(query: String): String {
|
||||||
val col = "c[]"
|
val col = "c[]"
|
||||||
return Uri.parse(baseUrl).buildUpon()
|
return Uri.parse(baseUrl).buildUpon()
|
||||||
.appendPath("manga.php")
|
.appendPath("manga.php")
|
||||||
.appendQueryParameter("q", query)
|
.appendQueryParameter("q", query)
|
||||||
.appendQueryParameter(col, "a")
|
.appendQueryParameter(col, "a")
|
||||||
.appendQueryParameter(col, "b")
|
.appendQueryParameter(col, "b")
|
||||||
.appendQueryParameter(col, "c")
|
.appendQueryParameter(col, "c")
|
||||||
.appendQueryParameter(col, "d")
|
.appendQueryParameter(col, "d")
|
||||||
.appendQueryParameter(col, "e")
|
.appendQueryParameter(col, "e")
|
||||||
.appendQueryParameter(col, "g")
|
.appendQueryParameter(col, "g")
|
||||||
.toString()
|
.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun exportListUrl() = Uri.parse(baseUrl).buildUpon()
|
private fun exportListUrl() = Uri.parse(baseUrl).buildUpon()
|
||||||
.appendPath("panel.php")
|
.appendPath("panel.php")
|
||||||
.appendQueryParameter("go", "export")
|
.appendQueryParameter("go", "export")
|
||||||
.toString()
|
.toString()
|
||||||
|
|
||||||
private fun updateUrl() = Uri.parse(baseModifyListUrl).buildUpon()
|
private fun updateUrl() = Uri.parse(baseModifyListUrl).buildUpon()
|
||||||
.appendPath("edit.json")
|
.appendPath("edit.json")
|
||||||
.toString()
|
.toString()
|
||||||
|
|
||||||
private fun addUrl() = Uri.parse(baseModifyListUrl).buildUpon()
|
private fun addUrl() = Uri.parse(baseModifyListUrl).buildUpon()
|
||||||
.appendPath( "add.json")
|
.appendPath("add.json")
|
||||||
.toString()
|
.toString()
|
||||||
|
|
||||||
private fun listEntryUrl(mediaId: Int) = Uri.parse(baseModifyListUrl).buildUpon()
|
private fun listEntryUrl(mediaId: Int) = Uri.parse(baseModifyListUrl).buildUpon()
|
||||||
.appendPath(mediaId.toString())
|
.appendPath(mediaId.toString())
|
||||||
.appendPath("edit")
|
.appendPath("edit")
|
||||||
.toString()
|
.toString()
|
||||||
|
|
||||||
private fun loginPostBody(username: String, password: String, csrf: String): RequestBody {
|
private fun loginPostBody(username: String, password: String, csrf: String): RequestBody {
|
||||||
return FormBody.Builder()
|
return FormBody.Builder()
|
||||||
.add("user_name", username)
|
.add("user_name", username)
|
||||||
.add("password", password)
|
.add("password", password)
|
||||||
.add("cookie", "1")
|
.add("cookie", "1")
|
||||||
.add("sublogin", "Login")
|
.add("sublogin", "Login")
|
||||||
.add("submit", "1")
|
.add("submit", "1")
|
||||||
.add(CSRF, csrf)
|
.add(CSRF, csrf)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun exportPostBody(): RequestBody {
|
private fun exportPostBody(): RequestBody {
|
||||||
return FormBody.Builder()
|
return FormBody.Builder()
|
||||||
.add("type", "2")
|
.add("type", "2")
|
||||||
.add("subexport", "Export My List")
|
.add("subexport", "Export My List")
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun mangaPostPayload(track: Track): RequestBody {
|
private fun mangaPostPayload(track: Track): RequestBody {
|
||||||
val body = JSONObject()
|
val body = JSONObject()
|
||||||
.put("manga_id", track.media_id)
|
.put("manga_id", track.media_id)
|
||||||
.put("status", track.status)
|
.put("status", track.status)
|
||||||
.put("score", track.score)
|
.put("score", track.score)
|
||||||
.put("num_read_chapters", track.last_chapter_read)
|
.put("num_read_chapters", track.last_chapter_read)
|
||||||
|
|
||||||
return body.toString().toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
|
return body.toString()
|
||||||
|
.toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Element.searchTitle() = select("strong").text()!!
|
private fun Element.searchTitle() = select("strong").text()!!
|
||||||
|
|
||||||
private fun Element.searchTotalChapters() = if (select(TD)[4].text() == "-") 0 else select(TD)[4].text().toInt()
|
private fun Element.searchTotalChapters() =
|
||||||
|
if (select(TD)[4].text() == "-") 0 else select(TD)[4].text().toInt()
|
||||||
|
|
||||||
private fun Element.searchCoverUrl() = select("img")
|
private fun Element.searchCoverUrl() = select("img")
|
||||||
.attr("data-src")
|
.attr("data-src")
|
||||||
.split("\\?")[0]
|
.split("\\?")[0]
|
||||||
.replace("/r/50x70/", "/")
|
.replace("/r/50x70/", "/")
|
||||||
|
|
||||||
private fun Element.searchMediaId() = select("div.picSurround")
|
private fun Element.searchMediaId() = select("div.picSurround")
|
||||||
.select("a").attr("id")
|
.select("a").attr("id")
|
||||||
.replace("sarea", "")
|
.replace("sarea", "")
|
||||||
.toInt()
|
.toInt()
|
||||||
|
|
||||||
private fun Element.searchSummary() = select("div.pt4")
|
private fun Element.searchSummary() = select("div.pt4")
|
||||||
.first()
|
.first()
|
||||||
.ownText()!!
|
.ownText()!!
|
||||||
|
|
||||||
private fun Element.searchPublishingStatus() = if (select(TD).last().text() == "-") "Publishing" else "Finished"
|
private fun Element.searchPublishingStatus() =
|
||||||
|
if (select(TD).last().text() == "-") "Publishing" else "Finished"
|
||||||
|
|
||||||
private fun Element.searchPublishingType() = select(TD)[2].text()!!
|
private fun Element.searchPublishingType() = select(TD)[2].text()!!
|
||||||
|
|
||||||
@ -300,6 +277,6 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
|||||||
"Dropped" -> 4
|
"Dropped" -> 4
|
||||||
"Plan to Read" -> 6
|
"Plan to Read" -> 6
|
||||||
else -> 1
|
else -> 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.myanimelist
|
package eu.kanade.tachiyomi.data.track.myanimelist
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
@ -8,20 +12,17 @@ import okhttp3.Response
|
|||||||
import okio.Buffer
|
import okio.Buffer
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
|
||||||
class MyAnimeListInterceptor(private val myanimelist: Myanimelist): Interceptor {
|
class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor {
|
||||||
|
|
||||||
|
val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
myanimelist.ensureLoggedIn()
|
scope.launch {
|
||||||
|
myanimelist.ensureLoggedIn()
|
||||||
val request = chain.request()
|
|
||||||
var response = chain.proceed(updateRequest(request))
|
|
||||||
|
|
||||||
if (response.code == 400) {
|
|
||||||
myanimelist.refreshLogin()
|
|
||||||
response = chain.proceed(updateRequest(request))
|
|
||||||
}
|
}
|
||||||
|
val request = chain.request()
|
||||||
|
return chain.proceed(updateRequest(request))
|
||||||
|
|
||||||
return response
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateRequest(request: Request): Request {
|
private fun updateRequest(request: Request): Request {
|
||||||
@ -46,13 +47,15 @@ class MyAnimeListInterceptor(private val myanimelist: Myanimelist): Interceptor
|
|||||||
private fun updateFormBody(requestBody: RequestBody): RequestBody {
|
private fun updateFormBody(requestBody: RequestBody): RequestBody {
|
||||||
val formString = bodyToString(requestBody)
|
val formString = bodyToString(requestBody)
|
||||||
|
|
||||||
return "$formString${if (formString.isNotEmpty()) "&" else ""}${MyAnimeListApi.CSRF}=${myanimelist.getCSRF()}".toRequestBody(requestBody.contentType())
|
return "$formString${if (formString.isNotEmpty()) "&" else ""}${MyAnimeListApi.CSRF}=${myanimelist.getCSRF()}".toRequestBody(
|
||||||
|
requestBody.contentType()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateJsonBody(requestBody: RequestBody): RequestBody {
|
private fun updateJsonBody(requestBody: RequestBody): RequestBody {
|
||||||
val jsonString = bodyToString(requestBody)
|
val jsonString = bodyToString(requestBody)
|
||||||
val newBody = JSONObject(jsonString)
|
val newBody = JSONObject(jsonString)
|
||||||
.put(MyAnimeListApi.CSRF, myanimelist.getCSRF())
|
.put(MyAnimeListApi.CSRF, myanimelist.getCSRF())
|
||||||
|
|
||||||
return newBody.toString().toRequestBody(requestBody.contentType())
|
return newBody.toString().toRequestBody(requestBody.contentType())
|
||||||
}
|
}
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.shikimori
|
|
||||||
|
|
||||||
data class OAuth(
|
|
||||||
val access_token: String,
|
|
||||||
val token_type: String,
|
|
||||||
val created_at: Long,
|
|
||||||
val expires_in: Long,
|
|
||||||
val refresh_token: String?) {
|
|
||||||
|
|
||||||
// Access token lives 1 day
|
|
||||||
fun isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600)
|
|
||||||
}
|
|
||||||
|
|
@ -7,74 +7,11 @@ import eu.kanade.tachiyomi.R
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import rx.Completable
|
import timber.log.Timber
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class Shikimori(private val context: Context, id: Int) : TrackService(id) {
|
class Shikimori(private val context: Context, id: Int) : TrackService(id) {
|
||||||
|
|
||||||
override fun getScoreList(): List<String> {
|
|
||||||
return IntRange(0, 10).map(Int::toString)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun displayScore(track: Track): String {
|
|
||||||
return track.score.toInt().toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun add(track: Track): Observable<Track> {
|
|
||||||
return api.addLibManga(track, getUsername())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun update(track: Track): Observable<Track> {
|
|
||||||
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
|
||||||
track.status = COMPLETED
|
|
||||||
}
|
|
||||||
return api.updateLibManga(track, getUsername())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun bind(track: Track): Observable<Track> {
|
|
||||||
return api.findLibManga(track, getUsername())
|
|
||||||
.flatMap { remoteTrack ->
|
|
||||||
if (remoteTrack != null) {
|
|
||||||
track.copyPersonalFrom(remoteTrack)
|
|
||||||
track.library_id = remoteTrack.library_id
|
|
||||||
update(track)
|
|
||||||
} else {
|
|
||||||
// Set default fields if it's not found in the list
|
|
||||||
track.score = DEFAULT_SCORE.toFloat()
|
|
||||||
track.status = DEFAULT_STATUS
|
|
||||||
add(track)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun search(query: String): Observable<List<TrackSearch>> {
|
|
||||||
return api.search(query)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun refresh(track: Track): Observable<Track> {
|
|
||||||
return api.findLibManga(track, getUsername())
|
|
||||||
.map { remoteTrack ->
|
|
||||||
if (remoteTrack != null) {
|
|
||||||
track.copyPersonalFrom(remoteTrack)
|
|
||||||
track.total_chapters = remoteTrack.total_chapters
|
|
||||||
}
|
|
||||||
track
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val READING = 1
|
|
||||||
const val COMPLETED = 2
|
|
||||||
const val ON_HOLD = 3
|
|
||||||
const val DROPPED = 4
|
|
||||||
const val PLANNING = 5
|
|
||||||
const val REPEATING = 6
|
|
||||||
|
|
||||||
const val DEFAULT_STATUS = READING
|
|
||||||
const val DEFAULT_SCORE = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override val name = "Shikimori"
|
override val name = "Shikimori"
|
||||||
|
|
||||||
private val gson: Gson by injectLazy()
|
private val gson: Gson by injectLazy()
|
||||||
@ -103,18 +40,64 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun login(username: String, password: String) = login(password)
|
override fun getScoreList(): List<String> {
|
||||||
|
return IntRange(0, 10).map(Int::toString)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun displayScore(track: Track): String {
|
||||||
|
return track.score.toInt().toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun update(track: Track): Track {
|
||||||
|
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||||
|
track.status = COMPLETED
|
||||||
|
}
|
||||||
|
return api.updateLibManga(track, getUsername())
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun bind(track: Track): Track {
|
||||||
|
val remoteTrack = api.findLibManga(track, getUsername())
|
||||||
|
|
||||||
|
if (remoteTrack != null) {
|
||||||
|
track.copyPersonalFrom(remoteTrack)
|
||||||
|
track.library_id = remoteTrack.library_id
|
||||||
|
update(track)
|
||||||
|
} else {
|
||||||
|
// Set default fields if it's not found in the list
|
||||||
|
track.score = DEFAULT_SCORE.toFloat()
|
||||||
|
track.status = DEFAULT_STATUS
|
||||||
|
return api.addLibManga(track, getUsername())
|
||||||
|
}
|
||||||
|
return track
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun search(query: String) = api.search(query)
|
||||||
|
|
||||||
|
override suspend fun refresh(track: Track): Track {
|
||||||
|
val remoteTrack = api.findLibManga(track, getUsername())
|
||||||
|
|
||||||
|
if (remoteTrack != null) {
|
||||||
|
track.copyPersonalFrom(remoteTrack)
|
||||||
|
track.total_chapters = remoteTrack.total_chapters
|
||||||
|
}
|
||||||
|
return track
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun login(username: String, password: String) = login(password)
|
||||||
|
|
||||||
|
suspend fun login(code: String): Boolean {
|
||||||
|
try {
|
||||||
|
val oauth = api.accessToken(code)
|
||||||
|
|
||||||
fun login(code: String): Completable {
|
|
||||||
return api.accessToken(code).map { oauth: OAuth? ->
|
|
||||||
interceptor.newAuth(oauth)
|
interceptor.newAuth(oauth)
|
||||||
if (oauth != null) {
|
val user = api.getCurrentUser()
|
||||||
val user = api.getCurrentUser()
|
saveCredentials(user.toString(), oauth.access_token)
|
||||||
saveCredentials(user.toString(), oauth.access_token)
|
return true
|
||||||
}
|
} catch (e: java.lang.Exception) {
|
||||||
}.doOnError {
|
Timber.e(e)
|
||||||
logout()
|
logout()
|
||||||
}.toCompletable()
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveToken(oauth: OAuth?) {
|
fun saveToken(oauth: OAuth?) {
|
||||||
@ -135,4 +118,16 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
preferences.trackToken(this).set(null)
|
preferences.trackToken(this).set(null)
|
||||||
interceptor.newAuth(null)
|
interceptor.newAuth(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val READING = 1
|
||||||
|
const val COMPLETED = 2
|
||||||
|
const val ON_HOLD = 3
|
||||||
|
const val DROPPED = 4
|
||||||
|
const val PLANNING = 5
|
||||||
|
const val REPEATING = 6
|
||||||
|
|
||||||
|
const val DEFAULT_STATUS = READING
|
||||||
|
const val DEFAULT_SCORE = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,68 +14,67 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
|||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import okhttp3.*
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.FormBody
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody
|
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInterceptor) {
|
class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInterceptor) {
|
||||||
|
|
||||||
private val gson: Gson by injectLazy()
|
private val gson: Gson by injectLazy()
|
||||||
private val parser = JsonParser()
|
|
||||||
private val jsonime = "application/json; charset=utf-8".toMediaTypeOrNull()
|
private val jsonime = "application/json; charset=utf-8".toMediaTypeOrNull()
|
||||||
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||||
|
|
||||||
fun addLibManga(track: Track, user_id: String): Observable<Track> {
|
suspend fun updateLibManga(track: Track, user_id: String): Track = addLibManga(track, user_id)
|
||||||
val payload = jsonObject(
|
|
||||||
|
suspend fun addLibManga(track: Track, user_id: String): Track {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
val payload = jsonObject(
|
||||||
"user_rate" to jsonObject(
|
"user_rate" to jsonObject(
|
||||||
"user_id" to user_id,
|
"user_id" to user_id,
|
||||||
"target_id" to track.media_id,
|
"target_id" to track.media_id,
|
||||||
"target_type" to "Manga",
|
"target_type" to "Manga",
|
||||||
"chapters" to track.last_chapter_read,
|
"chapters" to track.last_chapter_read,
|
||||||
"score" to track.score.toInt(),
|
"score" to track.score.toInt(),
|
||||||
"status" to track.toShikimoriStatus()
|
"status" to track.toShikimoriStatus()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val body = payload.toString().toRequestBody(jsonime)
|
val body = payload.toString().toRequestBody(jsonime)
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url("$apiUrl/v2/user_rates")
|
.url("$apiUrl/v2/user_rates")
|
||||||
.post(body)
|
.post(body)
|
||||||
.build()
|
.build()
|
||||||
return authClient.newCall(request)
|
authClient.newCall(request).execute()
|
||||||
.asObservableSuccess()
|
track
|
||||||
.map {
|
}
|
||||||
track
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateLibManga(track: Track, user_id: String): Observable<Track> = addLibManga(track, user_id)
|
suspend fun search(search: String): List<TrackSearch> {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
fun search(search: String): Observable<List<TrackSearch>> {
|
val url = Uri.parse("$apiUrl/mangas").buildUpon()
|
||||||
val url = Uri.parse("$apiUrl/mangas").buildUpon()
|
|
||||||
.appendQueryParameter("order", "popularity")
|
.appendQueryParameter("order", "popularity")
|
||||||
.appendQueryParameter("search", search)
|
.appendQueryParameter("search", search)
|
||||||
.appendQueryParameter("limit", "20")
|
.appendQueryParameter("limit", "20")
|
||||||
.build()
|
.build()
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url(url.toString())
|
.url(url.toString())
|
||||||
.get()
|
.get()
|
||||||
.build()
|
.build()
|
||||||
return authClient.newCall(request)
|
val netResponse = authClient.newCall(request).execute()
|
||||||
.asObservableSuccess()
|
|
||||||
.map { netResponse ->
|
|
||||||
val responseBody = netResponse.body?.string().orEmpty()
|
|
||||||
if (responseBody.isEmpty()) {
|
|
||||||
throw Exception("Null Response")
|
|
||||||
}
|
|
||||||
val response = parser.parse(responseBody).array
|
|
||||||
response.map { jsonToSearch(it.obj) }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
val responseBody = netResponse.body?.string().orEmpty()
|
||||||
|
if (responseBody.isEmpty()) {
|
||||||
|
throw Exception("Null Response")
|
||||||
|
}
|
||||||
|
val response = JsonParser.parseString(responseBody).array
|
||||||
|
|
||||||
|
response.map { jsonToSearch(it.obj) }
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun jsonToSearch(obj: JsonObject): TrackSearch {
|
private fun jsonToSearch(obj: JsonObject): TrackSearch {
|
||||||
@ -104,56 +103,55 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findLibManga(track: Track, user_id: String): Observable<Track?> {
|
suspend fun findLibManga(track: Track, user_id: String): Track? {
|
||||||
val url = Uri.parse("$apiUrl/v2/user_rates").buildUpon()
|
return withContext(Dispatchers.IO) {
|
||||||
|
val url = Uri.parse("$apiUrl/v2/user_rates").buildUpon()
|
||||||
.appendQueryParameter("user_id", user_id)
|
.appendQueryParameter("user_id", user_id)
|
||||||
.appendQueryParameter("target_id", track.media_id.toString())
|
.appendQueryParameter("target_id", track.media_id.toString())
|
||||||
.appendQueryParameter("target_type", "Manga")
|
.appendQueryParameter("target_type", "Manga")
|
||||||
.build()
|
.build()
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url(url.toString())
|
.url(url.toString())
|
||||||
.get()
|
.get()
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val urlMangas = Uri.parse("$apiUrl/mangas").buildUpon()
|
val urlMangas = Uri.parse("$apiUrl/mangas").buildUpon()
|
||||||
.appendPath(track.media_id.toString())
|
.appendPath(track.media_id.toString())
|
||||||
.build()
|
.build()
|
||||||
val requestMangas = Request.Builder()
|
val requestMangas = Request.Builder()
|
||||||
.url(urlMangas.toString())
|
.url(urlMangas.toString())
|
||||||
.get()
|
.get()
|
||||||
.build()
|
.build()
|
||||||
return authClient.newCall(requestMangas)
|
|
||||||
.asObservableSuccess()
|
val requestMangasResponse = authClient.newCall(requestMangas).execute()
|
||||||
.map { netResponse ->
|
val requestMangasBody = requestMangasResponse.body?.string().orEmpty()
|
||||||
val responseBody = netResponse.body?.string().orEmpty()
|
val mangas = JsonParser.parseString(requestMangasBody).obj
|
||||||
parser.parse(responseBody).obj
|
|
||||||
}.flatMap { mangas ->
|
val requestResponse = authClient.newCall(request).execute()
|
||||||
authClient.newCall(request)
|
val requestResponseBody = requestResponse.body?.string().orEmpty()
|
||||||
.asObservableSuccess()
|
|
||||||
.map { netResponse ->
|
if (requestResponseBody.isEmpty()) {
|
||||||
val responseBody = netResponse.body?.string().orEmpty()
|
throw Exception("Null Response")
|
||||||
if (responseBody.isEmpty()) {
|
}
|
||||||
throw Exception("Null Response")
|
val response = JsonParser.parseString(requestResponseBody).array
|
||||||
}
|
if (response.size() > 1) {
|
||||||
val response = parser.parse(responseBody).array
|
throw Exception("Too much mangas in response")
|
||||||
if (response.size() > 1) {
|
}
|
||||||
throw Exception("Too much mangas in response")
|
val entry = response.map {
|
||||||
}
|
jsonToTrack(it.obj, mangas)
|
||||||
val entry = response.map {
|
}
|
||||||
jsonToTrack(it.obj, mangas)
|
entry.firstOrNull()
|
||||||
}
|
}
|
||||||
entry.firstOrNull()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCurrentUser(): Int {
|
fun getCurrentUser(): Int {
|
||||||
val user = authClient.newCall(GET("$apiUrl/users/whoami")).execute().body?.string()
|
val user = authClient.newCall(GET("$apiUrl/users/whoami")).execute().body?.string()
|
||||||
return parser.parse(user).obj["id"].asInt
|
return JsonParser.parseString(user).obj["id"].asInt
|
||||||
}
|
}
|
||||||
|
|
||||||
fun accessToken(code: String): Observable<OAuth> {
|
suspend fun accessToken(code: String): OAuth {
|
||||||
return client.newCall(accessTokenRequest(code)).asObservableSuccess().map { netResponse ->
|
return withContext(Dispatchers.IO) {
|
||||||
|
val netResponse= client.newCall(accessTokenRequest(code)).execute()
|
||||||
val responseBody = netResponse.body?.string().orEmpty()
|
val responseBody = netResponse.body?.string().orEmpty()
|
||||||
if (responseBody.isEmpty()) {
|
if (responseBody.isEmpty()) {
|
||||||
throw Exception("Null Response")
|
throw Exception("Null Response")
|
||||||
@ -162,20 +160,22 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun accessTokenRequest(code: String) = POST(oauthUrl,
|
private fun accessTokenRequest(code: String) = POST(
|
||||||
body = FormBody.Builder()
|
oauthUrl,
|
||||||
.add("grant_type", "authorization_code")
|
body = FormBody.Builder()
|
||||||
.add("client_id", clientId)
|
.add("grant_type", "authorization_code")
|
||||||
.add("client_secret", clientSecret)
|
.add("client_id", clientId)
|
||||||
.add("code", code)
|
.add("client_secret", clientSecret)
|
||||||
.add("redirect_uri", redirectUrl)
|
.add("code", code)
|
||||||
.build()
|
.add("redirect_uri", redirectUrl)
|
||||||
|
.build()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val clientId = "1aaf4cf232372708e98b5abc813d795b539c5a916dbbfe9ac61bf02a360832cc"
|
private const val clientId =
|
||||||
private const val clientSecret = "229942c742dd4cde803125d17d64501d91c0b12e14cb1e5120184d77d67024c0"
|
"1aaf4cf232372708e98b5abc813d795b539c5a916dbbfe9ac61bf02a360832cc"
|
||||||
|
private const val clientSecret =
|
||||||
|
"229942c742dd4cde803125d17d64501d91c0b12e14cb1e5120184d77d67024c0"
|
||||||
|
|
||||||
private const val baseUrl = "https://shikimori.one"
|
private const val baseUrl = "https://shikimori.one"
|
||||||
private const val apiUrl = "https://shikimori.one/api"
|
private const val apiUrl = "https://shikimori.one/api"
|
||||||
@ -190,21 +190,20 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun authUrl() =
|
fun authUrl() =
|
||||||
Uri.parse(loginUrl).buildUpon()
|
Uri.parse(loginUrl).buildUpon()
|
||||||
.appendQueryParameter("client_id", clientId)
|
.appendQueryParameter("client_id", clientId)
|
||||||
.appendQueryParameter("redirect_uri", redirectUrl)
|
.appendQueryParameter("redirect_uri", redirectUrl)
|
||||||
.appendQueryParameter("response_type", "code")
|
.appendQueryParameter("response_type", "code")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
|
||||||
fun refreshTokenRequest(token: String) = POST(oauthUrl,
|
|
||||||
body = FormBody.Builder()
|
|
||||||
.add("grant_type", "refresh_token")
|
|
||||||
.add("client_id", clientId)
|
|
||||||
.add("client_secret", clientSecret)
|
|
||||||
.add("refresh_token", token)
|
|
||||||
.build())
|
|
||||||
|
|
||||||
|
fun refreshTokenRequest(token: String) = POST(
|
||||||
|
oauthUrl,
|
||||||
|
body = FormBody.Builder()
|
||||||
|
.add("grant_type", "refresh_token")
|
||||||
|
.add("client_id", clientId)
|
||||||
|
.add("client_secret", clientSecret)
|
||||||
|
.add("refresh_token", token)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -22,3 +22,15 @@ fun toTrackStatus(status: String) = when (status) {
|
|||||||
|
|
||||||
else -> throw Exception("Unknown status")
|
else -> throw Exception("Unknown status")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class OAuth(
|
||||||
|
val access_token: String,
|
||||||
|
val token_type: String,
|
||||||
|
val created_at: Long,
|
||||||
|
val expires_in: Long,
|
||||||
|
val refresh_token: String?) {
|
||||||
|
|
||||||
|
// Access token lives 1 day
|
||||||
|
fun isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -1,20 +1,13 @@
|
|||||||
package eu.kanade.tachiyomi.data.updater
|
package eu.kanade.tachiyomi.data.updater
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.data.updater.devrepo.DevRepoUpdateChecker
|
|
||||||
import eu.kanade.tachiyomi.data.updater.github.GithubUpdateChecker
|
import eu.kanade.tachiyomi.data.updater.github.GithubUpdateChecker
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
abstract class UpdateChecker {
|
abstract class UpdateChecker {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getUpdateChecker(): UpdateChecker {
|
fun getUpdateChecker(): UpdateChecker = GithubUpdateChecker()
|
||||||
return if (BuildConfig.DEBUG) {
|
|
||||||
DevRepoUpdateChecker()
|
|
||||||
} else {
|
|
||||||
GithubUpdateChecker()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.updater.devrepo
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.updater.Release
|
|
||||||
|
|
||||||
class DevRepoRelease(override val info: String) : Release {
|
|
||||||
|
|
||||||
override val downloadLink: String
|
|
||||||
get() = LATEST_URL
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val LATEST_URL = "https://tachiyomi.kanade.eu/latest"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.updater.devrepo
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdateChecker
|
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdateResult
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
|
||||||
import eu.kanade.tachiyomi.network.asObservable
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
class DevRepoUpdateChecker : UpdateChecker() {
|
|
||||||
|
|
||||||
private val client: OkHttpClient by lazy {
|
|
||||||
Injekt.get<NetworkHelper>().client.newBuilder()
|
|
||||||
.followRedirects(false)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val versionRegex: Regex by lazy {
|
|
||||||
Regex("tachiyomi-r(\\d+).apk")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun checkForUpdate(): Observable<UpdateResult> {
|
|
||||||
return client.newCall(GET(DevRepoRelease.LATEST_URL)).asObservable()
|
|
||||||
.map { response ->
|
|
||||||
// Get latest repo version number from header in format "Location: tachiyomi-r1512.apk"
|
|
||||||
val latestVersionNumber: String = versionRegex.find(response.header("Location")!!)!!.groupValues[1]
|
|
||||||
|
|
||||||
if (latestVersionNumber.toInt() > BuildConfig.COMMIT_COUNT.toInt()) {
|
|
||||||
DevRepoUpdateResult.NewUpdate(DevRepoRelease("v$latestVersionNumber"))
|
|
||||||
} else {
|
|
||||||
DevRepoUpdateResult.NoNewUpdate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.updater.devrepo
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdateResult
|
|
||||||
|
|
||||||
sealed class DevRepoUpdateResult : UpdateResult() {
|
|
||||||
|
|
||||||
class NewUpdate(release: DevRepoRelease): UpdateResult.NewUpdate<DevRepoRelease>(release)
|
|
||||||
class NoNewUpdate: UpdateResult.NoNewUpdate()
|
|
||||||
|
|
||||||
}
|
|
@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.data.updater.github
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory
|
|
||||||
import retrofit2.converter.gson.GsonConverterFactory
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
@ -19,7 +18,6 @@ interface GithubService {
|
|||||||
val restAdapter = Retrofit.Builder()
|
val restAdapter = Retrofit.Builder()
|
||||||
.baseUrl("https://api.github.com")
|
.baseUrl("https://api.github.com")
|
||||||
.addConverterFactory(GsonConverterFactory.create())
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
|
||||||
.client(Injekt.get<NetworkHelper>().client)
|
.client(Injekt.get<NetworkHelper>().client)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
@ -28,6 +26,6 @@ interface GithubService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GET("/repos/Jays2Kings/tachiyomiJ2K/releases/latest")
|
@GET("/repos/Jays2Kings/tachiyomiJ2K/releases/latest")
|
||||||
fun getLatestVersion(): Observable<GithubRelease>
|
suspend fun getLatestVersion(): GithubRelease
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,15 @@ package eu.kanade.tachiyomi.network
|
|||||||
|
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import okhttp3.*
|
import okhttp3.*
|
||||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Producer
|
import rx.Producer
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
|
import java.io.BufferedReader
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.io.InputStreamReader
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import java.util.zip.GZIPInputStream
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.resumeWithException
|
import kotlin.coroutines.resumeWithException
|
||||||
|
|
||||||
@ -94,3 +98,25 @@ fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListene
|
|||||||
|
|
||||||
return progressClient.newCall(request)
|
return progressClient.newCall(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun MediaType.Companion.jsonType() : MediaType = "application/json; charset=utf-8".toMediaTypeOrNull()!!
|
||||||
|
|
||||||
|
fun Response.consumeBody(): String? {
|
||||||
|
use {
|
||||||
|
if (it.code != 200) throw Exception("HTTP error ${it.code}")
|
||||||
|
return it.body?.string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Response.consumeXmlBody(): String? {
|
||||||
|
use { res ->
|
||||||
|
if (res.code != 200) throw Exception("Export list error")
|
||||||
|
BufferedReader(InputStreamReader(GZIPInputStream(res.body?.source()?.inputStream()))).use { reader ->
|
||||||
|
val sb = StringBuilder()
|
||||||
|
reader.forEachLine { line ->
|
||||||
|
sb.append(line)
|
||||||
|
}
|
||||||
|
return sb.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -101,6 +101,7 @@ import jp.wasabeef.glide.transformations.MaskTransformation
|
|||||||
import kotlinx.android.synthetic.main.main_activity.*
|
import kotlinx.android.synthetic.main.main_activity.*
|
||||||
import kotlinx.android.synthetic.main.manga_details_controller.*
|
import kotlinx.android.synthetic.main.manga_details_controller.*
|
||||||
import kotlinx.android.synthetic.main.manga_header_item.*
|
import kotlinx.android.synthetic.main.manga_header_item.*
|
||||||
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -927,10 +928,12 @@ class MangaDetailsController : BaseController,
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun trackRefreshError(error: Exception) {
|
fun trackRefreshError(error: Exception) {
|
||||||
|
Timber.e(error)
|
||||||
trackingBottomSheet?.onRefreshError(error)
|
trackingBottomSheet?.onRefreshError(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun trackSearchError(error: Exception) {
|
fun trackSearchError(error: Exception) {
|
||||||
|
Timber.e(error)
|
||||||
trackingBottomSheet?.onSearchResultsError(error)
|
trackingBottomSheet?.onSearchResultsError(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -691,7 +691,7 @@ class MangaDetailsPresenter(private val controller: MangaDetailsController,
|
|||||||
val list = trackList.filter { it.track != null }.map { item ->
|
val list = trackList.filter { it.track != null }.map { item ->
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val trackItem = try {
|
val trackItem = try {
|
||||||
item.service.refresh(item.track!!).toBlocking().single()
|
item.service.refresh(item.track!!)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
trackError(e)
|
trackError(e)
|
||||||
null
|
null
|
||||||
@ -710,7 +710,7 @@ class MangaDetailsPresenter(private val controller: MangaDetailsController,
|
|||||||
|
|
||||||
fun trackSearch(query: String, service: TrackService) {
|
fun trackSearch(query: String, service: TrackService) {
|
||||||
launch(Dispatchers.IO) {
|
launch(Dispatchers.IO) {
|
||||||
val results = try {service.search(query).toBlocking().single() }
|
val results = try {service.search(query) }
|
||||||
catch (e: Exception) {
|
catch (e: Exception) {
|
||||||
withContext(Dispatchers.Main) { controller.trackSearchError(e) }
|
withContext(Dispatchers.Main) { controller.trackSearchError(e) }
|
||||||
null }
|
null }
|
||||||
@ -725,7 +725,7 @@ class MangaDetailsPresenter(private val controller: MangaDetailsController,
|
|||||||
item.manga_id = manga.id!!
|
item.manga_id = manga.id!!
|
||||||
|
|
||||||
launch {
|
launch {
|
||||||
val binding = try { service.bind(item).toBlocking().single() }
|
val binding = try { service.bind(item) }
|
||||||
catch (e: Exception) {
|
catch (e: Exception) {
|
||||||
trackError(e)
|
trackError(e)
|
||||||
null
|
null
|
||||||
@ -745,7 +745,7 @@ class MangaDetailsPresenter(private val controller: MangaDetailsController,
|
|||||||
|
|
||||||
private fun updateRemote(track: Track, service: TrackService) {
|
private fun updateRemote(track: Track, service: TrackService) {
|
||||||
launch {
|
launch {
|
||||||
val binding = try { service.update(track).toBlocking().single() }
|
val binding = try { service.update(track) }
|
||||||
catch (e: Exception) {
|
catch (e: Exception) {
|
||||||
trackError(e)
|
trackError(e)
|
||||||
null
|
null
|
||||||
|
@ -24,6 +24,11 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
|||||||
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import rx.Completable
|
import rx.Completable
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
@ -40,11 +45,11 @@ import java.util.concurrent.TimeUnit
|
|||||||
* Presenter used by the activity to perform background operations.
|
* Presenter used by the activity to perform background operations.
|
||||||
*/
|
*/
|
||||||
class ReaderPresenter(
|
class ReaderPresenter(
|
||||||
private val db: DatabaseHelper = Injekt.get(),
|
private val db: DatabaseHelper = Injekt.get(),
|
||||||
private val sourceManager: SourceManager = Injekt.get(),
|
private val sourceManager: SourceManager = Injekt.get(),
|
||||||
private val downloadManager: DownloadManager = Injekt.get(),
|
private val downloadManager: DownloadManager = Injekt.get(),
|
||||||
private val coverCache: CoverCache = Injekt.get(),
|
private val coverCache: CoverCache = Injekt.get(),
|
||||||
private val preferences: PreferencesHelper = Injekt.get()
|
private val preferences: PreferencesHelper = Injekt.get()
|
||||||
) : BasePresenter<ReaderActivity>() {
|
) : BasePresenter<ReaderActivity>() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -87,19 +92,19 @@ class ReaderPresenter(
|
|||||||
val dbChapters = db.getChapters(manga).executeAsBlocking()
|
val dbChapters = db.getChapters(manga).executeAsBlocking()
|
||||||
|
|
||||||
val selectedChapter = dbChapters.find { it.id == chapterId }
|
val selectedChapter = dbChapters.find { it.id == chapterId }
|
||||||
?: error("Requested chapter of id $chapterId not found in chapter list")
|
?: error("Requested chapter of id $chapterId not found in chapter list")
|
||||||
|
|
||||||
val chaptersForReader =
|
val chaptersForReader =
|
||||||
if (preferences.skipRead()) {
|
if (preferences.skipRead()) {
|
||||||
val list = dbChapters.filter { !it.read }.toMutableList()
|
val list = dbChapters.filter { !it.read }.toMutableList()
|
||||||
val find = list.find { it.id == chapterId }
|
val find = list.find { it.id == chapterId }
|
||||||
if (find == null) {
|
if (find == null) {
|
||||||
list.add(selectedChapter)
|
list.add(selectedChapter)
|
||||||
}
|
|
||||||
list
|
|
||||||
} else {
|
|
||||||
dbChapters
|
|
||||||
}
|
}
|
||||||
|
list
|
||||||
|
} else {
|
||||||
|
dbChapters
|
||||||
|
}
|
||||||
|
|
||||||
when (manga.sorting) {
|
when (manga.sorting) {
|
||||||
Manga.SORTING_SOURCE -> ChapterLoadBySource().get(chaptersForReader)
|
Manga.SORTING_SOURCE -> ChapterLoadBySource().get(chaptersForReader)
|
||||||
@ -170,12 +175,12 @@ class ReaderPresenter(
|
|||||||
if (!needsInit()) return
|
if (!needsInit()) return
|
||||||
|
|
||||||
db.getManga(mangaId).asRxObservable()
|
db.getManga(mangaId).asRxObservable()
|
||||||
.first()
|
.first()
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.doOnNext { init(it, initialChapterId) }
|
.doOnNext { init(it, initialChapterId) }
|
||||||
.subscribeFirst({ _, _ ->
|
.subscribeFirst({ _, _ ->
|
||||||
// Ignore onNext event
|
// Ignore onNext event
|
||||||
}, ReaderActivity::setInitialChapterError)
|
}, ReaderActivity::setInitialChapterError)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun init(mangaId: Long, chapterUrl: String) {
|
fun init(mangaId: Long, chapterUrl: String) {
|
||||||
@ -207,13 +212,13 @@ class ReaderPresenter(
|
|||||||
// Read chapterList from an io thread because it's retrieved lazily and would block main.
|
// Read chapterList from an io thread because it's retrieved lazily and would block main.
|
||||||
activeChapterSubscription?.unsubscribe()
|
activeChapterSubscription?.unsubscribe()
|
||||||
activeChapterSubscription = Observable
|
activeChapterSubscription = Observable
|
||||||
.fromCallable { chapterList.first { chapterId == it.chapter.id } }
|
.fromCallable { chapterList.first { chapterId == it.chapter.id } }
|
||||||
.flatMap { getLoadObservable(loader!!, it) }
|
.flatMap { getLoadObservable(loader!!, it) }
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribeFirst({ _, _ ->
|
.subscribeFirst({ _, _ ->
|
||||||
// Ignore onNext event
|
// Ignore onNext event
|
||||||
}, ReaderActivity::setInitialChapterError)
|
}, ReaderActivity::setInitialChapterError)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -224,27 +229,29 @@ class ReaderPresenter(
|
|||||||
* Callers must also handle the onError event.
|
* Callers must also handle the onError event.
|
||||||
*/
|
*/
|
||||||
private fun getLoadObservable(
|
private fun getLoadObservable(
|
||||||
loader: ChapterLoader,
|
loader: ChapterLoader,
|
||||||
chapter: ReaderChapter
|
chapter: ReaderChapter
|
||||||
): Observable<ViewerChapters> {
|
): Observable<ViewerChapters> {
|
||||||
return loader.loadChapter(chapter)
|
return loader.loadChapter(chapter)
|
||||||
.andThen(Observable.fromCallable {
|
.andThen(Observable.fromCallable {
|
||||||
val chapterPos = chapterList.indexOf(chapter)
|
val chapterPos = chapterList.indexOf(chapter)
|
||||||
|
|
||||||
ViewerChapters(chapter,
|
ViewerChapters(
|
||||||
chapterList.getOrNull(chapterPos - 1),
|
chapter,
|
||||||
chapterList.getOrNull(chapterPos + 1))
|
chapterList.getOrNull(chapterPos - 1),
|
||||||
})
|
chapterList.getOrNull(chapterPos + 1)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
)
|
||||||
.doOnNext { newChapters ->
|
})
|
||||||
val oldChapters = viewerChaptersRelay.value
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.doOnNext { newChapters ->
|
||||||
|
val oldChapters = viewerChaptersRelay.value
|
||||||
|
|
||||||
// Add new references first to avoid unnecessary recycling
|
// Add new references first to avoid unnecessary recycling
|
||||||
newChapters.ref()
|
newChapters.ref()
|
||||||
oldChapters?.unref()
|
oldChapters?.unref()
|
||||||
|
|
||||||
viewerChaptersRelay.call(newChapters)
|
viewerChaptersRelay.call(newChapters)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -258,10 +265,10 @@ class ReaderPresenter(
|
|||||||
|
|
||||||
activeChapterSubscription?.unsubscribe()
|
activeChapterSubscription?.unsubscribe()
|
||||||
activeChapterSubscription = getLoadObservable(loader, chapter)
|
activeChapterSubscription = getLoadObservable(loader, chapter)
|
||||||
.toCompletable()
|
.toCompletable()
|
||||||
.onErrorComplete()
|
.onErrorComplete()
|
||||||
.subscribe()
|
.subscribe()
|
||||||
.also(::add)
|
.also(::add)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -276,13 +283,13 @@ class ReaderPresenter(
|
|||||||
|
|
||||||
activeChapterSubscription?.unsubscribe()
|
activeChapterSubscription?.unsubscribe()
|
||||||
activeChapterSubscription = getLoadObservable(loader, chapter)
|
activeChapterSubscription = getLoadObservable(loader, chapter)
|
||||||
.doOnSubscribe { isLoadingAdjacentChapterRelay.call(true) }
|
.doOnSubscribe { isLoadingAdjacentChapterRelay.call(true) }
|
||||||
.doOnUnsubscribe { isLoadingAdjacentChapterRelay.call(false) }
|
.doOnUnsubscribe { isLoadingAdjacentChapterRelay.call(false) }
|
||||||
.subscribeFirst({ view, _ ->
|
.subscribeFirst({ view, _ ->
|
||||||
view.moveToPageIndex(0)
|
view.moveToPageIndex(0)
|
||||||
}, { _, _ ->
|
}, { _, _ ->
|
||||||
// Ignore onError event, viewers handle that state
|
// Ignore onError event, viewers handle that state
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -299,12 +306,12 @@ class ReaderPresenter(
|
|||||||
val loader = loader ?: return
|
val loader = loader ?: return
|
||||||
|
|
||||||
loader.loadChapter(chapter)
|
loader.loadChapter(chapter)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
// Update current chapters whenever a chapter is preloaded
|
// Update current chapters whenever a chapter is preloaded
|
||||||
.doOnCompleted { viewerChaptersRelay.value?.let(viewerChaptersRelay::call) }
|
.doOnCompleted { viewerChaptersRelay.value?.let(viewerChaptersRelay::call) }
|
||||||
.onErrorComplete()
|
.onErrorComplete()
|
||||||
.subscribe()
|
.subscribe()
|
||||||
.also(::add)
|
.also(::add)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -348,9 +355,9 @@ class ReaderPresenter(
|
|||||||
*/
|
*/
|
||||||
private fun saveChapterProgress(chapter: ReaderChapter) {
|
private fun saveChapterProgress(chapter: ReaderChapter) {
|
||||||
db.updateChapterProgress(chapter.chapter).asRxCompletable()
|
db.updateChapterProgress(chapter.chapter).asRxCompletable()
|
||||||
.onErrorComplete()
|
.onErrorComplete()
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.subscribe()
|
.subscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -412,18 +419,18 @@ class ReaderPresenter(
|
|||||||
db.updateMangaViewer(manga).executeAsBlocking()
|
db.updateMangaViewer(manga).executeAsBlocking()
|
||||||
|
|
||||||
Observable.timer(250, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
|
Observable.timer(250, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
|
||||||
.subscribeFirst({ view, _ ->
|
.subscribeFirst({ view, _ ->
|
||||||
val currChapters = viewerChaptersRelay.value
|
val currChapters = viewerChaptersRelay.value
|
||||||
if (currChapters != null) {
|
if (currChapters != null) {
|
||||||
// Save current page
|
// Save current page
|
||||||
val currChapter = currChapters.currChapter
|
val currChapter = currChapters.currChapter
|
||||||
currChapter.requestedPage = currChapter.chapter.last_page_read
|
currChapter.requestedPage = currChapter.chapter.last_page_read
|
||||||
|
|
||||||
// Emit manga and chapters to the new viewer
|
// Emit manga and chapters to the new viewer
|
||||||
view.setManga(manga)
|
view.setManga(manga)
|
||||||
view.setChapters(currChapters)
|
view.setChapters(currChapters)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -439,7 +446,7 @@ class ReaderPresenter(
|
|||||||
|
|
||||||
// Build destination file.
|
// Build destination file.
|
||||||
val filename = DiskUtil.buildValidFilename(
|
val filename = DiskUtil.buildValidFilename(
|
||||||
"${manga.currentTitle()} - ${chapter.name}".take(225)
|
"${manga.currentTitle()} - ${chapter.name}".take(225)
|
||||||
) + " - ${page.number}.${type.extension}"
|
) + " - ${page.number}.${type.extension}"
|
||||||
|
|
||||||
val destFile = File(directory, filename)
|
val destFile = File(directory, filename)
|
||||||
@ -464,23 +471,25 @@ class ReaderPresenter(
|
|||||||
notifier.onClear()
|
notifier.onClear()
|
||||||
|
|
||||||
// Pictures directory.
|
// Pictures directory.
|
||||||
val destDir = File(Environment.getExternalStorageDirectory().absolutePath +
|
val destDir = File(
|
||||||
|
Environment.getExternalStorageDirectory().absolutePath +
|
||||||
File.separator + Environment.DIRECTORY_PICTURES +
|
File.separator + Environment.DIRECTORY_PICTURES +
|
||||||
File.separator + "Tachiyomi")
|
File.separator + "Tachiyomi"
|
||||||
|
)
|
||||||
|
|
||||||
// Copy file in background.
|
// Copy file in background.
|
||||||
Observable.fromCallable { saveImage(page, destDir, manga) }
|
Observable.fromCallable { saveImage(page, destDir, manga) }
|
||||||
.doOnNext { file ->
|
.doOnNext { file ->
|
||||||
DiskUtil.scanMedia(context, file)
|
DiskUtil.scanMedia(context, file)
|
||||||
notifier.onComplete(file)
|
notifier.onComplete(file)
|
||||||
}
|
}
|
||||||
.doOnError { notifier.onError(it.message) }
|
.doOnError { notifier.onError(it.message) }
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribeFirst(
|
.subscribeFirst(
|
||||||
{ view, file -> view.onSaveImageResult(SaveImageResult.Success(file)) },
|
{ view, file -> view.onSaveImageResult(SaveImageResult.Success(file)) },
|
||||||
{ view, error -> view.onSaveImageResult(SaveImageResult.Error(error)) }
|
{ view, error -> view.onSaveImageResult(SaveImageResult.Error(error)) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -498,13 +507,13 @@ class ReaderPresenter(
|
|||||||
val destDir = File(context.cacheDir, "shared_image")
|
val destDir = File(context.cacheDir, "shared_image")
|
||||||
|
|
||||||
Observable.fromCallable { destDir.deleteRecursively() } // Keep only the last shared file
|
Observable.fromCallable { destDir.deleteRecursively() } // Keep only the last shared file
|
||||||
.map { saveImage(page, destDir, manga) }
|
.map { saveImage(page, destDir, manga) }
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribeFirst(
|
.subscribeFirst(
|
||||||
{ view, file -> view.onShareImageResult(file) },
|
{ view, file -> view.onShareImageResult(file) },
|
||||||
{ _, _ -> /* Empty */ }
|
{ _, _ -> /* Empty */ }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -516,29 +525,29 @@ class ReaderPresenter(
|
|||||||
val stream = page.stream ?: return
|
val stream = page.stream ?: return
|
||||||
|
|
||||||
Observable
|
Observable
|
||||||
.fromCallable {
|
.fromCallable {
|
||||||
if (manga.source == LocalSource.ID) {
|
if (manga.source == LocalSource.ID) {
|
||||||
val context = Injekt.get<Application>()
|
val context = Injekt.get<Application>()
|
||||||
LocalSource.updateCover(context, manga, stream())
|
LocalSource.updateCover(context, manga, stream())
|
||||||
R.string.cover_updated
|
R.string.cover_updated
|
||||||
|
SetAsCoverResult.Success
|
||||||
|
} else {
|
||||||
|
val thumbUrl = manga.thumbnail_url ?: throw Exception("Image url not found")
|
||||||
|
if (manga.favorite) {
|
||||||
|
coverCache.copyToCache(thumbUrl, stream())
|
||||||
|
MangaImpl.setLastCoverFetch(manga.id!!, Date().time)
|
||||||
SetAsCoverResult.Success
|
SetAsCoverResult.Success
|
||||||
} else {
|
} else {
|
||||||
val thumbUrl = manga.thumbnail_url ?: throw Exception("Image url not found")
|
SetAsCoverResult.AddToLibraryFirst
|
||||||
if (manga.favorite) {
|
|
||||||
coverCache.copyToCache(thumbUrl, stream())
|
|
||||||
MangaImpl.setLastCoverFetch(manga.id!!, Date().time)
|
|
||||||
SetAsCoverResult.Success
|
|
||||||
} else {
|
|
||||||
SetAsCoverResult.AddToLibraryFirst
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.subscribeOn(Schedulers.io())
|
}
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.subscribeOn(Schedulers.io())
|
||||||
.subscribeFirst(
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
{ view, result -> view.onSetAsCoverResult(result) },
|
.subscribeFirst(
|
||||||
{ view, _ -> view.onSetAsCoverResult(SetAsCoverResult.Error) }
|
{ view, result -> view.onSetAsCoverResult(result) },
|
||||||
)
|
{ view, _ -> view.onSetAsCoverResult(SetAsCoverResult.Error) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -568,27 +577,24 @@ class ReaderPresenter(
|
|||||||
|
|
||||||
val trackManager = Injekt.get<TrackManager>()
|
val trackManager = Injekt.get<TrackManager>()
|
||||||
|
|
||||||
db.getTracks(manga).asRxSingle()
|
// We wan't these to execute even if the presenter is destroyed so launch on GlobalScope
|
||||||
.flatMapCompletable { trackList ->
|
GlobalScope.launch {
|
||||||
Completable.concat(trackList.map { track ->
|
withContext(Dispatchers.IO) {
|
||||||
val service = trackManager.getService(track.sync_id)
|
val trackList = db.getTracks(manga).executeAsBlocking()
|
||||||
if (service != null && service.isLogged && chapterRead > track.last_chapter_read) {
|
trackList.map { track ->
|
||||||
|
val service = trackManager.getService(track.sync_id)
|
||||||
|
if (service != null && service.isLogged && chapterRead > track.last_chapter_read) {
|
||||||
|
try {
|
||||||
track.last_chapter_read = chapterRead
|
track.last_chapter_read = chapterRead
|
||||||
|
service.update(track)
|
||||||
// We wan't these to execute even if the presenter is destroyed and leaks
|
db.insertTrack(track).executeAsBlocking()
|
||||||
// for a while. The view can still be garbage collected.
|
} catch (e: Exception) {
|
||||||
Observable.defer { service.update(track) }
|
Timber.e(e)
|
||||||
.map { db.insertTrack(track).executeAsBlocking() }
|
|
||||||
.toCompletable()
|
|
||||||
.onErrorComplete()
|
|
||||||
} else {
|
|
||||||
Completable.complete()
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
.onErrorComplete()
|
}
|
||||||
.subscribeOn(Schedulers.io())
|
}
|
||||||
.subscribe()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -604,19 +610,19 @@ class ReaderPresenter(
|
|||||||
if (removeAfterReadSlots == -1) return
|
if (removeAfterReadSlots == -1) return
|
||||||
|
|
||||||
Completable
|
Completable
|
||||||
.fromCallable {
|
.fromCallable {
|
||||||
// Position of the read chapter
|
// Position of the read chapter
|
||||||
val position = chapterList.indexOf(chapter)
|
val position = chapterList.indexOf(chapter)
|
||||||
|
|
||||||
// Retrieve chapter to delete according to preference
|
// Retrieve chapter to delete according to preference
|
||||||
val chapterToDelete = chapterList.getOrNull(position - removeAfterReadSlots)
|
val chapterToDelete = chapterList.getOrNull(position - removeAfterReadSlots)
|
||||||
if (chapterToDelete != null) {
|
if (chapterToDelete != null) {
|
||||||
downloadManager.enqueueDeleteChapters(listOf(chapterToDelete.chapter), manga)
|
downloadManager.enqueueDeleteChapters(listOf(chapterToDelete.chapter), manga)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.onErrorComplete()
|
}
|
||||||
.subscribeOn(Schedulers.io())
|
.onErrorComplete()
|
||||||
.subscribe()
|
.subscribeOn(Schedulers.io())
|
||||||
|
.subscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -625,9 +631,8 @@ class ReaderPresenter(
|
|||||||
*/
|
*/
|
||||||
private fun deletePendingChapters() {
|
private fun deletePendingChapters() {
|
||||||
Completable.fromCallable { downloadManager.deletePendingChapters() }
|
Completable.fromCallable { downloadManager.deletePendingChapters() }
|
||||||
.onErrorComplete()
|
.onErrorComplete()
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.subscribe()
|
.subscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2,21 +2,26 @@ package eu.kanade.tachiyomi.ui.setting.track
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import android.view.Gravity.CENTER
|
import android.view.Gravity.CENTER
|
||||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import rx.schedulers.Schedulers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class AnilistLoginActivity : AppCompatActivity() {
|
class AnilistLoginActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private val trackManager: TrackManager by injectLazy()
|
private val trackManager: TrackManager by injectLazy()
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
super.onCreate(savedState)
|
super.onCreate(savedState)
|
||||||
|
|
||||||
@ -26,20 +31,21 @@ class AnilistLoginActivity : AppCompatActivity() {
|
|||||||
val regex = "(?:access_token=)(.*?)(?:&)".toRegex()
|
val regex = "(?:access_token=)(.*?)(?:&)".toRegex()
|
||||||
val matchResult = regex.find(intent.data?.fragment.toString())
|
val matchResult = regex.find(intent.data?.fragment.toString())
|
||||||
if (matchResult?.groups?.get(1) != null) {
|
if (matchResult?.groups?.get(1) != null) {
|
||||||
trackManager.aniList.login(matchResult.groups[1]!!.value)
|
scope.launch {
|
||||||
.subscribeOn(Schedulers.io())
|
trackManager.aniList.login(matchResult.groups[1]!!.value)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
returnToSettings()
|
||||||
.subscribe({
|
}
|
||||||
returnToSettings()
|
|
||||||
}, {
|
|
||||||
returnToSettings()
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
trackManager.aniList.logout()
|
trackManager.aniList.logout()
|
||||||
returnToSettings()
|
returnToSettings()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
scope.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
private fun returnToSettings() {
|
private fun returnToSettings() {
|
||||||
finish()
|
finish()
|
||||||
|
|
||||||
@ -47,5 +53,4 @@ class AnilistLoginActivity : AppCompatActivity() {
|
|||||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2,13 +2,18 @@ package eu.kanade.tachiyomi.ui.setting.track
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import android.view.Gravity.CENTER
|
import android.view.Gravity.CENTER
|
||||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
@ -17,6 +22,8 @@ class BangumiLoginActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private val trackManager: TrackManager by injectLazy()
|
private val trackManager: TrackManager by injectLazy()
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
super.onCreate(savedState)
|
super.onCreate(savedState)
|
||||||
|
|
||||||
@ -25,14 +32,10 @@ class BangumiLoginActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
val code = intent.data?.getQueryParameter("code")
|
val code = intent.data?.getQueryParameter("code")
|
||||||
if (code != null) {
|
if (code != null) {
|
||||||
trackManager.bangumi.login(code)
|
scope.launch {
|
||||||
.subscribeOn(Schedulers.io())
|
trackManager.bangumi.login(code)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
returnToSettings()
|
||||||
.subscribe({
|
}
|
||||||
returnToSettings()
|
|
||||||
}, {
|
|
||||||
returnToSettings()
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
trackManager.bangumi.logout()
|
trackManager.bangumi.logout()
|
||||||
returnToSettings()
|
returnToSettings()
|
||||||
@ -46,5 +49,4 @@ class BangumiLoginActivity : AppCompatActivity() {
|
|||||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2,21 +2,25 @@ package eu.kanade.tachiyomi.ui.setting.track
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import android.view.Gravity.CENTER
|
import android.view.Gravity.CENTER
|
||||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import rx.schedulers.Schedulers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class ShikimoriLoginActivity : AppCompatActivity() {
|
class ShikimoriLoginActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private val trackManager: TrackManager by injectLazy()
|
private val trackManager: TrackManager by injectLazy()
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
super.onCreate(savedState)
|
super.onCreate(savedState)
|
||||||
|
|
||||||
@ -25,14 +29,10 @@ class ShikimoriLoginActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
val code = intent.data?.getQueryParameter("code")
|
val code = intent.data?.getQueryParameter("code")
|
||||||
if (code != null) {
|
if (code != null) {
|
||||||
trackManager.shikimori.login(code)
|
scope.launch {
|
||||||
.subscribeOn(Schedulers.io())
|
trackManager.shikimori.login(code)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
returnToSettings()
|
||||||
.subscribe({
|
}
|
||||||
returnToSettings()
|
|
||||||
}, {
|
|
||||||
returnToSettings()
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
trackManager.shikimori.logout()
|
trackManager.shikimori.logout()
|
||||||
returnToSettings()
|
returnToSettings()
|
||||||
@ -46,5 +46,4 @@ class ShikimoriLoginActivity : AppCompatActivity() {
|
|||||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -13,21 +13,27 @@ import eu.kanade.tachiyomi.R
|
|||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
import eu.kanade.tachiyomi.widget.SimpleTextWatcher
|
import eu.kanade.tachiyomi.widget.SimpleTextWatcher
|
||||||
import kotlinx.android.synthetic.main.pref_account_login.view.login
|
import kotlinx.android.synthetic.main.pref_account_login.view.*
|
||||||
import kotlinx.android.synthetic.main.pref_account_login.view.password
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.android.synthetic.main.pref_account_login.view.show_password
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.android.synthetic.main.pref_account_login.view.username_label
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
abstract class LoginDialogPreference(private val usernameLabel: String? = null, bundle: Bundle? = null) :
|
abstract class LoginDialogPreference(
|
||||||
DialogController(bundle) {
|
private val usernameLabel: String? = null,
|
||||||
|
bundle: Bundle? = null
|
||||||
|
) :
|
||||||
|
DialogController(bundle) {
|
||||||
|
|
||||||
var v: View? = null
|
var v: View? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
val preferences: PreferencesHelper by injectLazy()
|
val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||||
|
|
||||||
var requestSubscription: Subscription? = null
|
var requestSubscription: Subscription? = null
|
||||||
|
|
||||||
open var canLogout = false
|
open var canLogout = false
|
||||||
@ -46,7 +52,7 @@ abstract class LoginDialogPreference(private val usernameLabel: String? = null,
|
|||||||
return dialog
|
return dialog
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun logout() { }
|
open fun logout() {}
|
||||||
|
|
||||||
fun onViewCreated(view: View) {
|
fun onViewCreated(view: View) {
|
||||||
v = view.apply {
|
v = view.apply {
|
||||||
@ -76,7 +82,6 @@ abstract class LoginDialogPreference(private val usernameLabel: String? = null,
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
||||||
@ -87,11 +92,11 @@ abstract class LoginDialogPreference(private val usernameLabel: String? = null,
|
|||||||
}
|
}
|
||||||
|
|
||||||
open fun onDialogClosed() {
|
open fun onDialogClosed() {
|
||||||
|
scope.cancel()
|
||||||
requestSubscription?.unsubscribe()
|
requestSubscription?.unsubscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract fun checkLogin()
|
protected abstract fun checkLogin()
|
||||||
|
|
||||||
protected abstract fun setCredentialsOnView(view: View)
|
protected abstract fun setCredentialsOnView(view: View)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -7,13 +7,12 @@ import eu.kanade.tachiyomi.data.track.TrackManager
|
|||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import kotlinx.android.synthetic.main.pref_account_login.view.*
|
import kotlinx.android.synthetic.main.pref_account_login.view.*
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import kotlinx.coroutines.launch
|
||||||
import rx.schedulers.Schedulers
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class TrackLoginDialog(usernameLabel: String? = null, bundle: Bundle? = null) :
|
class TrackLoginDialog(usernameLabel: String? = null, bundle: Bundle? = null) :
|
||||||
LoginDialogPreference(usernameLabel, bundle) {
|
LoginDialogPreference(usernameLabel, bundle) {
|
||||||
|
|
||||||
private val service = Injekt.get<TrackManager>().getService(args.getInt("key"))!!
|
private val service = Injekt.get<TrackManager>().getService(args.getInt("key"))!!
|
||||||
|
|
||||||
@ -22,7 +21,7 @@ class TrackLoginDialog(usernameLabel: String? = null, bundle: Bundle? = null) :
|
|||||||
constructor(service: TrackService) : this(service, null)
|
constructor(service: TrackService) : this(service, null)
|
||||||
|
|
||||||
constructor(service: TrackService, usernameLabel: String?) :
|
constructor(service: TrackService, usernameLabel: String?) :
|
||||||
this(usernameLabel, Bundle().apply { putInt("key", service.id) })
|
this(usernameLabel, Bundle().apply { putInt("key", service.id) })
|
||||||
|
|
||||||
override fun setCredentialsOnView(view: View) = with(view) {
|
override fun setCredentialsOnView(view: View) = with(view) {
|
||||||
dialog_title.text = context.getString(R.string.login_title, service.name)
|
dialog_title.text = context.getString(R.string.login_title, service.name)
|
||||||
@ -31,7 +30,6 @@ class TrackLoginDialog(usernameLabel: String? = null, bundle: Bundle? = null) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun checkLogin() {
|
override fun checkLogin() {
|
||||||
requestSubscription?.unsubscribe()
|
|
||||||
|
|
||||||
v?.apply {
|
v?.apply {
|
||||||
if (username.text.isEmpty() || password.text.isEmpty())
|
if (username.text.isEmpty() || password.text.isEmpty())
|
||||||
@ -41,17 +39,27 @@ class TrackLoginDialog(usernameLabel: String? = null, bundle: Bundle? = null) :
|
|||||||
val user = username.text.toString()
|
val user = username.text.toString()
|
||||||
val pass = password.text.toString()
|
val pass = password.text.toString()
|
||||||
|
|
||||||
requestSubscription = service.login(user, pass)
|
scope.launch {
|
||||||
.subscribeOn(Schedulers.io())
|
try {
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
val result = service.login(user, pass)
|
||||||
.subscribe({
|
if (result) {
|
||||||
dialog?.dismiss()
|
dialog?.dismiss()
|
||||||
context.toast(R.string.login_success)
|
context.toast(R.string.login_success)
|
||||||
}, { error ->
|
} else {
|
||||||
login.progress = -1
|
errorResult(this@apply)
|
||||||
login.setText(R.string.unknown_error)
|
}
|
||||||
error.message?.let { context.toast(it) }
|
} catch (error: Exception) {
|
||||||
})
|
errorResult(this@apply)
|
||||||
|
error.message?.let { context.toast(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun errorResult(view: View?) {
|
||||||
|
v?.apply {
|
||||||
|
login.progress = -1
|
||||||
|
login.setText(R.string.unknown_error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,5 +78,4 @@ class TrackLoginDialog(usernameLabel: String? = null, bundle: Bundle? = null) :
|
|||||||
interface Listener {
|
interface Listener {
|
||||||
fun trackDialogClosed(service: TrackService)
|
fun trackDialogClosed(service: TrackService)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user