Compare commits

...

112 Commits

Author SHA1 Message Date
len
11262f86f9 Release 0.2.2 2016-06-06 14:43:02 +02:00
len
d1db2d60ec Fix #329. Add confirmation before removing favorite manga on multiple selection. Add new proguard rules 2016-06-05 23:40:27 +02:00
len
156e43290e Remove unneeded swipe refresh 2016-06-05 11:54:35 +02:00
len
6687c80b2d Minor changes 2016-06-05 11:14:40 +02:00
1fbec7bf3d Added improvements for RecentChapters. Closes #320 (#324) 2016-06-04 17:50:44 +02:00
len
6196480d1d Minor improvements 2016-06-03 18:25:47 +02:00
len
2723aeeb5c Better error feedback. Closes #325 2016-06-03 12:37:07 +02:00
len
728ab18017 Make active page nullable. Fixes #326 2016-06-03 12:25:39 +02:00
len
0714fdc7e6 Fix #328 2016-06-03 12:09:38 +02:00
7bfdfee27b Merge pull request #323 from NoodleMage/season
Added chapter recognition for "season" case
2016-06-02 16:46:26 +02:00
10ec3a9b0c Added chapter recognition for "season" case 2016-06-02 16:39:19 +02:00
eec728f162 Rename "Only update incomplete manga" string to "ongoing" (#318) 2016-06-02 14:42:40 +02:00
len
1ac8ef5341 Reader fixes and minor changes 2016-06-02 14:11:10 +02:00
len
18cdddf433 Minor improvements for sync services 2016-05-31 16:14:32 +02:00
len
02a697031f Chapters FAB doesn't depend on the recognized chapter number. Cloudflare resolver fix 2016-05-28 19:10:09 +02:00
len
6beff242b0 LoginSource moved to an interface 2016-05-28 00:20:59 +02:00
len
46cc078e93 Downloading chapters now always add them from the beginning 2016-05-27 20:11:37 +02:00
len
9aa6da0642 Default headers are bad 2016-05-27 18:08:05 +02:00
len
8bd20c39aa All network calls are now done directly through the client 2016-05-27 17:17:30 +02:00
len
efd36388b0 Fix network unsubscription crashes, refactor network methods 2016-05-27 16:52:44 +02:00
len
79bb207a8d Use async method for network calls, trying to fix a crash 2016-05-27 13:56:18 +02:00
len
0fe350af9a Minor classes restructuration and optimize imports 2016-05-27 13:21:21 +02:00
len
4e784cd7c3 Minor fixes 2016-05-26 16:03:55 +02:00
len
12d6919421 Do not parse -2 chapter number 2016-05-26 15:52:12 +02:00
len
79ec4faddb Always close requests' response body 2016-05-26 15:16:42 +02:00
6603c0b990 Merge pull request #239 from inorichi/rewrite-source
Rewrite source
2016-05-26 15:14:36 +02:00
len
b5dbdd1774 Change default sorting method 2016-05-26 15:10:31 +02:00
len
a08cea9df8 Show changelog when new version is installed 2016-05-26 15:10:28 +02:00
5d9c817461 Implement ReadMangaToday with the new source 2016-05-26 10:22:43 +02:00
f95c9a12c9 Reimplement russian sources 2016-05-26 10:22:43 +02:00
len
015257fe75 Implement Mangafox and Mangahere with the new source 2016-05-26 10:22:42 +02:00
len
dd5692bb2d Rewrite sources. Implement Batoto and Kissmanga 2016-05-26 10:22:39 +02:00
len
bc3d5dc863 Add support's library custom tabs 2016-05-25 17:33:58 +02:00
a4b6003e58 Wrong versionName when not building latest version (#314) 2016-05-25 16:10:03 +02:00
8086d1db46 squid:SwitchLastCaseIsDefaultCheck - switch statements should end with a default clause (#306) 2016-05-25 00:09:38 +02:00
26f4f53ec2 Fixed spinner + improved ripple (#310) 2016-05-25 00:09:24 +02:00
len
6af78418a4 Fix an issue with seamless mode and chapters with less than 5 pages #291 2016-05-23 12:10:24 +02:00
len
f629db3c10 Exclude manga from unknown sources from the library 2016-05-22 23:40:44 +02:00
len
af0cf9e52d Load next/prev chapter depending on the sorting method 2016-05-21 22:04:07 +02:00
len
e885469504 Allow to change sorting mode from the chapters fragment 2016-05-21 22:04:06 +02:00
len
ca7e5260f0 Tables classes in Kotlin. Rename COLUMN -> COL 2016-05-21 22:04:06 +02:00
len
dba64f849b Database support for ordering chapters like the source 2016-05-21 22:04:05 +02:00
02e43bafd6 Merge pull request #303 from NoodleMage/myanimelist_cardview
UI tweaks
2016-05-21 17:45:49 +02:00
len
637dda2e22 Make status bar transparent on API >= 21 2016-05-21 15:43:48 +02:00
575eaee1d2 UI tweaks 2016-05-19 16:29:25 +02:00
48a1e8f74c pmd:UseIndexOfChar - Use Index Of Char (#300) 2016-05-17 21:49:13 +02:00
len
4d65038ad3 Optimize imports 2016-05-15 23:06:11 +02:00
len
6e8a41f898 Kissmanga loading through Cloudflare. A lot of refactoring was needed 2016-05-15 20:46:58 +02:00
len
8da11dbdb9 Fix MAL not binding a manga. Upgrade Kotlin to 1.0.2 2016-05-13 13:37:08 +02:00
len
70fabf6a6b Bump dependencies and support library 2016-05-12 20:15:15 +02:00
len
43fafbc747 Fix a crash when trying to change the downloads directory on some devices 2016-05-12 16:45:35 +02:00
len
88e64c878b Double the distance required to trigger category update and remove overflow menu setting 2016-05-12 15:30:57 +02:00
0ad9e4af0b Fix #267 (#299) 2016-05-12 14:28:44 +02:00
a6df745daa Rewrote ChapterRecognition to Kotlin. (#293) 2016-05-11 15:33:14 +02:00
len
c64bd81339 Build debug in travis 2016-05-10 16:13:28 +02:00
c31f0b4bb3 Change travis gradle task, small doc update (#295)
* Use gradle task build in travis

* Update CONTRIBUTING.md
2016-05-10 13:42:08 +02:00
len
136136d055 Trying workaround for #296 2016-05-09 13:29:46 +02:00
len
c20d86e5c0 Category update can only be triggered when the list is at the top 2016-05-09 13:23:57 +02:00
len
7ca99f749b Remember last active category. Closes #261 2016-05-08 18:11:09 +02:00
len
7cc4405c09 Swipe down updates active category. Closes #292. Not sure I'll keep this 2016-05-08 17:27:49 +02:00
len
62d5deaa6f Allow to open manga in the browser. Closes #157 (Doesn't work with kissmanga) 2016-05-08 15:20:55 +02:00
len
7f5879ed6f Add confirm dialog for multiple deletion. Closes #155 2016-05-08 14:58:21 +02:00
len
a0f7761a37 Minor changes 2016-05-07 23:50:05 +02:00
ed77c60283 Added download notifications, resolves #260 (#289) 2016-05-07 23:09:14 +02:00
len
8f144316a6 Fix a crash when updating active category and no categories available 2016-05-06 15:44:01 +02:00
len
e73eed4a9b Fix catalog covers' flickering when adding a page 2016-05-06 14:28:39 +02:00
len
9de3da33aa Fix recent chapters menu button not showing on high dpi screens 2016-05-05 20:58:19 +02:00
len
0de214c3b5 Delete from the download queue on the main thread. It could fix some crashes 2016-05-05 16:05:36 +02:00
len
1226023dc2 Allow to update one category 2016-05-05 00:37:03 +02:00
len
5e24054a0b Database queries are now separated by table. Improve how the app creates downloads 2016-05-02 22:36:10 +02:00
af2b886599 Fix F-Droid not recognizing update (#287) 2016-05-02 14:53:43 +02:00
len
1d1e5f1f99 Toggle reader menu with the menu button, closes #286. Fix incorrect drawer selected item when using back button 2016-05-01 21:53:00 +02:00
len
49628e9cf5 Fix recent crashes 2016-04-30 01:44:53 +02:00
len
47bc1f7a9f Remove RelativeLayout from covers for better performance 2016-04-29 20:00:03 +02:00
len
f9783407bd Downgrade RxJava. Closes #285 2016-04-29 14:26:30 +02:00
len
74ffa14304 Bump dependencies 2016-04-29 00:22:41 +02:00
len
e881488bcc Refactor and convert to Kotlin base classes. Fix FAB behavior 2016-04-28 21:54:54 +02:00
97ee7b81af Release version manual update hotfix. (#283) 2016-04-28 18:46:12 +02:00
ff6eefe1c4 pmd:ImmutableField - Immutable Field (#282) 2016-04-28 18:46:05 +02:00
9f546d13c2 squid:S1118 - Utility classes should not have public constructors (#281) 2016-04-28 18:45:39 +02:00
2e6fc70353 Auto number of latest release, shorter versionCode (#280) 2016-04-27 22:18:36 +02:00
270cacb1d7 squid:S2039 - Member variable visibility should be specified (#279) 2016-04-27 22:18:25 +02:00
len
e2ecf0ce5f Release 0.2.1 2016-04-27 14:06:45 +02:00
len
5d396bfb7c Make query non nullable, it fixes some bugs in the catalogue 2016-04-27 00:58:05 +02:00
len
de6cc8394e Fixed some crashes in the catalogue and the reader 2016-04-27 00:23:06 +02:00
4b7159648a Merge pull request #257 from NoodleMage/update_improv
Rewrote UpdateDownloader to Kotlin + AC fixes
2016-04-27 00:20:50 +02:00
eb9c5f95db Removed automatic update + duplicate fix 2016-04-26 22:56:49 +02:00
55e9d2880c Rewrote UpdateDownloader to Kotlin
Added auto update check (every 12 hour)
Warning message optional fix #256
Lots of bug fixes!
2016-04-26 20:57:05 +02:00
len
ec9c19ce7d Use a thread-safe list for downloads 2016-04-26 14:00:22 +02:00
len
31731e8f26 Fix a crash in older android versions 2016-04-25 23:43:32 +02:00
len
bfb12bc7c1 Minor changes to fix a possible crash in the downloads view 2016-04-24 23:32:49 +02:00
len
4befcf3819 Fix #277, library not updating 2016-04-23 15:39:41 +02:00
cb58145361 Allow setting versionCode in parameter (#276)
Allow easier debug versionCode change
2016-04-23 13:17:48 +02:00
len
b83efd90a8 Slightly increase library view performance by caching typefaces 2016-04-22 19:41:59 +02:00
len
9f0da3f1d6 Upgrade to nucleus 3 2016-04-22 14:28:06 +02:00
len
50ae08ed8d Back button now returns to library. Closes #252 2016-04-21 16:31:23 +02:00
len
5385642a5b Downloads now retry requests after some seconds. Closes #271 2016-04-21 15:57:47 +02:00
len
0a27d4e185 Add an option to reencode images under the advanced tab. #262 2016-04-21 15:31:07 +02:00
len
bd8b9febd2 Minor changes 2016-04-21 01:04:46 +02:00
len
a30705f197 Oops... nobody noticed being unlogged from batoto? 2016-04-20 17:31:31 +02:00
len
877032a757 Fix incomplete downloads. Closes #264 2016-04-20 17:10:10 +02:00
len
19bf47b6d2 Release resources before trying to delete an incomplete file #264 #211 2016-04-19 21:04:28 +02:00
len
a9bfeb058b Revert "Temporarily include nucleus in the project"
This reverts commit 447dfd1e3c.
2016-04-19 14:11:03 +02:00
len
9213fc6999 Always close response body 2016-04-19 14:08:35 +02:00
len
447dfd1e3c Temporarily include nucleus in the project 2016-04-19 12:58:33 +02:00
len
638d3a32cf Also use manga per row setting in catalogue 2016-04-18 20:33:09 +02:00
len
17c59657c3 Allow to unbind manga, closes #258. Fix some network calls leaking 2016-04-18 20:14:50 +02:00
len
81bce8ef76 Mark common categories when moving them. Closes #135 2016-04-18 19:20:14 +02:00
len
78314077bb Fix custom brightness turning off the screen. #106 2016-04-18 17:36:27 +02:00
len
a7840bc247 Rewrite PreferencesHelper. Allow to customize navigation with volume keys and tapping. Closes #251 and closes #129. 2016-04-18 17:29:46 +02:00
6d0254c5e5 Fixed backup/restore for 3rd party applications (#255)
* Same MIME for restore/backup, so 3rd party applications like Google Drive find the proper files.
MIME changed to proper type for json files.

* MIME type for restore temporaly on "application/*" so the cached file can also be chosen
2016-04-18 13:40:34 +02:00
06681a3db7 squid:S1854 - Dead stores should be removed (#253) 2016-04-18 13:40:26 +02:00
257 changed files with 8293 additions and 7013 deletions

View File

@ -1,19 +1,20 @@
# Bugs
* Include version (Setting > About > Version)
* If not latest, try updating, it may have already been solved
* Debug version is equal to the number of commits as seen in the main page
* Dev version is equal to the number of commits as seen in the main page
* Include steps to reproduce (if not obvious from description)
* Include screenshot (if needed)
* If it could be device-dependent, try reproducing on another device (if possible), include results and device names, OS, modifications (root, Xposed)
* If it could be device-dependent, try reproducing on another device (if possible), include results and device names, OS, modifications (root, Xposed)
* **Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).**
* For large logs use http://pastebin.com/ (or similar)
* For multipart issues use list like this:
* For multipart issues **use list** like this:
* [x] Done
* [ ] Not done
```
* [x] Done
* [ ] Not done
```
* Don't put together too many unrelated requests into one issue
DO: https://github.com/inorichi/tachiyomi/issues/24 https://github.com/inorichi/tachiyomi/issues/71

View File

@ -15,7 +15,7 @@ android:
before_script:
- chmod +x gradlew
#Build, and run tests
script: "./gradlew clean assembleDebug testDebugUnitTest"
script: "./gradlew clean buildDebug"
sudo: false
before_cache:
@ -25,4 +25,4 @@ cache:
- $HOME/.gradle/caches/
- $HOME/.gradle/wrapper/
env:
- GRADLE_OPTS="-XX:MaxPermSize=1024m -XX:+CMSClassUnloadingEnabled -XX:+HeapDumpOnOutOfMemoryError -Xmx2048m"
- GRADLE_OPTS="-XX:MaxPermSize=1024m -XX:+CMSClassUnloadingEnabled -XX:+HeapDumpOnOutOfMemoryError -Xmx2048m"

View File

@ -1,6 +1,6 @@
| Build | Download | Auto Update |
|-------|----------|-------------|
| [![TeamCity (simple build status)](https://img.shields.io/teamcity/https/teamcity.kanade.eu/s/tachiyomi_Build.svg)](https://teamcity.kanade.eu/project.html?projectId=tachiyomi) [![Travis](https://img.shields.io/travis/inorichi/tachiyomi.svg)](https://travis-ci.org/inorichi/tachiyomi) | [![stable release](https://img.shields.io/badge/stable-v0.2.0-blue.svg)](https://github.com/inorichi/tachiyomi/releases) [![latest dev build](https://img.shields.io/badge/dev-latest%20build-blue.svg)](http://tachiyomi.kanade.eu/latest/app-debug.apk) | [![fdroid release](https://img.shields.io/badge/stable-F--Droid-blue.svg)](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi) [![fdroid debug](https://img.shields.io/badge/dev-F--Droid-blue.svg)](//github.com/inorichi/tachiyomi/wiki/FDroid-for-debug-versions) |
| [![TeamCity (simple build status)](https://img.shields.io/teamcity/https/teamcity.kanade.eu/s/tachiyomi_Build.svg)](https://teamcity.kanade.eu/project.html?projectId=tachiyomi) [![Travis](https://img.shields.io/travis/inorichi/tachiyomi.svg)](https://travis-ci.org/inorichi/tachiyomi) | [![stable release](https://img.shields.io/github/release/inorichi/tachiyomi.svg?maxAge=2592000&label=stable)](https://github.com/inorichi/tachiyomi/releases) [![latest dev build](https://img.shields.io/badge/dev-latest%20build-blue.svg)](http://tachiyomi.kanade.eu/latest/app-debug.apk) | [![fdroid release](https://img.shields.io/badge/stable-F--Droid-blue.svg)](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi) [![fdroid debug](https://img.shields.io/badge/dev-F--Droid-blue.svg)](//github.com/inorichi/tachiyomi/wiki/FDroid-for-debug-versions) |
## [Report an issue](https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md)

View File

@ -8,7 +8,7 @@ ext {
// Git is needed in your system PATH for these commands to work.
// If it's not installed, you can return a random value as a workaround
getCommitCount = {
return 'git rev-list --count origin/master'.execute().text.trim()
return 'git rev-list --count HEAD'.execute().text.trim()
// return "1"
}
@ -38,8 +38,9 @@ android {
minSdkVersion 16
targetSdkVersion 23
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
versionCode 6
versionName "0.2.0"
versionCode 8
versionCode project.findProperty('versionCode')?.toInteger() ?: 8
versionName "0.2.2"
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
@ -88,10 +89,10 @@ kapt {
}
dependencies {
final SUPPORT_LIBRARY_VERSION = '23.3.0'
final DAGGER_VERSION = '2.2'
final OKHTTP_VERSION = '3.2.0'
final RETROFIT_VERSION = '2.0.1'
final SUPPORT_LIBRARY_VERSION = '23.4.0'
final DAGGER_VERSION = '2.4'
final RETROFIT_VERSION = '2.0.2'
final NUCLEUS_VERSION = '3.0.0'
final STORIO_VERSION = '1.8.0'
final MOCKITO_VERSION = '1.10.19'
@ -106,18 +107,17 @@ dependencies {
compile "com.android.support:design:$SUPPORT_LIBRARY_VERSION"
compile "com.android.support:recyclerview-v7:$SUPPORT_LIBRARY_VERSION"
compile "com.android.support:support-annotations:$SUPPORT_LIBRARY_VERSION"
compile "com.android.support:percent:$SUPPORT_LIBRARY_VERSION"
compile "com.android.support:preference-v7:$SUPPORT_LIBRARY_VERSION"
compile "com.android.support:preference-v14:$SUPPORT_LIBRARY_VERSION"
compile "com.android.support:customtabs:$SUPPORT_LIBRARY_VERSION"
// ReactiveX
compile 'io.reactivex:rxandroid:1.1.0'
compile 'io.reactivex:rxjava:1.1.1'
compile 'io.reactivex:rxandroid:1.2.0'
compile 'io.reactivex:rxjava:1.1.5'
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.1'
// Network client
compile "com.squareup.okhttp3:okhttp:$OKHTTP_VERSION"
compile "com.squareup.okhttp3:okhttp-urlconnection:$OKHTTP_VERSION"
compile "com.squareup.okhttp3:okhttp:3.3.1"
// REST
compile "com.squareup.retrofit2:retrofit:$RETROFIT_VERSION"
@ -125,16 +125,25 @@ dependencies {
compile "com.squareup.retrofit2:adapter-rxjava:$RETROFIT_VERSION"
// IO
compile 'com.squareup.okio:okio:1.7.0'
compile 'com.squareup.okio:okio:1.8.0'
// JSON
compile 'com.google.code.gson:gson:2.6.2'
// YAML
compile 'org.yaml:snakeyaml:1.17'
// JavaScript engine
compile 'com.squareup.duktape:duktape-android:0.9.5'
// Disk cache
compile 'com.jakewharton:disklrucache:2.0.2'
// Parse HTML
compile 'org.jsoup:jsoup:1.8.3'
compile 'org.jsoup:jsoup:1.9.2'
// Changelog
compile 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
// Database
compile "com.pushtorefresh.storio:sqlite:$STORIO_VERSION"
@ -142,7 +151,9 @@ dependencies {
kapt "com.pushtorefresh.storio:sqlite-annotations-processor:$STORIO_VERSION"
// Model View Presenter
compile 'info.android15.nucleus:nucleus:2.0.5'
compile "info.android15.nucleus:nucleus:$NUCLEUS_VERSION"
compile "info.android15.nucleus:nucleus-support-v4:$NUCLEUS_VERSION"
compile "info.android15.nucleus:nucleus-support-v7:$NUCLEUS_VERSION"
// Dependency injection
compile "com.google.dagger:dagger:$DAGGER_VERSION"
@ -151,6 +162,7 @@ dependencies {
// Image library
compile 'com.github.bumptech.glide:glide:3.7.0'
compile 'com.github.bumptech.glide:okhttp3-integration:1.4.0@aar'
// Logging
compile 'com.jakewharton.timber:timber:4.1.2'
@ -163,9 +175,7 @@ dependencies {
compile 'eu.davidea:flexible-adapter:4.2.0'
compile 'com.nononsenseapps:filepicker:2.5.2'
compile 'com.github.amulyakhare:TextDrawable:558677e'
compile('com.github.afollestad.material-dialogs:core:0.8.5.5@aar') {
transitive = true
}
compile 'com.afollestad.material-dialogs:core:0.8.5.9'
// Tests
testCompile 'junit:junit:4.12'
@ -181,7 +191,7 @@ dependencies {
}
buildscript {
ext.kotlin_version = '1.0.1'
ext.kotlin_version = '1.0.2'
repositories {
mavenCentral()
}

View File

@ -118,4 +118,12 @@
# Keep the support library
-keep class org.acra.** { *; }
-keep interface org.acra.** { *; }
-keep interface org.acra.** { *; }
# SnakeYaml
-keep class org.yaml.snakeyaml.** { public protected private *; }
-keep class org.yaml.snakeyaml.** { public protected private *; }
-dontwarn org.yaml.snakeyaml.**
# Duktape
-keep class com.squareup.duktape.** { *; }

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi"
xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.kanade.tachiyomi">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@ -68,7 +68,19 @@
</receiver>
<receiver
android:name=".data.library.LibraryUpdateService$LibraryUpdateReceiver">
android:name=".data.library.LibraryUpdateService$SyncOnPowerConnected"
android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.ACTION_POWER_CONNECTED" />
</intent-filter>
</receiver>
<receiver
android:name=".data.library.LibraryUpdateService$CancelUpdateReceiver">
</receiver>
<receiver
android:name=".data.updater.UpdateDownloader$InstallOnReceived">
</receiver>
<receiver
@ -79,8 +91,17 @@
</intent-filter>
</receiver>
<receiver
android:name=".data.updater.UpdateDownloaderAlarm">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="eu.kanade.CHECK_UPDATE"/>
</intent-filter>
</receiver>
<meta-data
android:name="eu.kanade.tachiyomi.data.cache.CoverGlideModule"
android:name="eu.kanade.tachiyomi.data.glide.AppGlideModule"
android:value="GlideModule" />
</application>

View File

@ -0,0 +1,8 @@
package eu.kanade.tachiyomi
object Constants {
const val NOTIFICATION_LIBRARY_ID = 1
const val NOTIFICATION_UPDATER_ID = 2
const val NOTIFICATION_DOWNLOAD_CHAPTER_ID = 3
const val NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID = 4
}

View File

@ -7,6 +7,7 @@ import com.google.gson.reflect.TypeToken
import com.jakewharton.disklrucache.DiskLruCache
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.util.DiskUtils
import eu.kanade.tachiyomi.util.saveImageTo
import okhttp3.Response
import okio.Okio
import rx.Observable
@ -184,7 +185,7 @@ class ChapterCache(private val context: Context) {
* @throws IOException image error.
*/
@Throws(IOException::class)
fun putImageToCache(imageUrl: String, response: Response) {
fun putImageToCache(imageUrl: String, response: Response, reencode: Boolean) {
// Initialize editor (edits the values for an entry).
var editor: DiskLruCache.Editor? = null
@ -194,17 +195,12 @@ class ChapterCache(private val context: Context) {
editor = diskCache.edit(key) ?: throw IOException("Unable to edit key")
// Get OutputStream and write image with Okio.
Okio.buffer(Okio.sink(editor.newOutputStream(0))).use {
it.writeAll(response.body().source())
it.flush()
}
response.body().source().saveImageTo(editor.newOutputStream(0), reencode)
diskCache.flush()
editor.commit()
} catch (e: Exception) {
response.body().close()
throw IOException("Unable to save image")
} finally {
response.body().close()
editor?.abortUnlessCommitted()
}
}

View File

@ -1,11 +1,6 @@
package eu.kanade.tachiyomi.data.cache
import android.content.Context
import com.bumptech.glide.Glide
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.model.LazyHeaders
import com.bumptech.glide.request.animation.GlideAnimation
import com.bumptech.glide.request.target.SimpleTarget
import eu.kanade.tachiyomi.util.DiskUtils
import java.io.File
import java.io.IOException
@ -27,80 +22,19 @@ class CoverCache(private val context: Context) {
*/
private val cacheDir: File = File(context.externalCacheDir, "cover_disk_cache")
/**
* Download the cover with Glide and save the file.
* @param thumbnailUrl url of thumbnail.
* @param headers headers included in Glide request.
* @param onReady function to call when the image is ready
*/
fun save(thumbnailUrl: String?, headers: LazyHeaders, onReady: ((File) -> Unit)? = null) {
// Check if url is empty.
if (thumbnailUrl.isNullOrEmpty())
return
// Download the cover with Glide and save the file.
val url = GlideUrl(thumbnailUrl, headers)
Glide.with(context)
.load(url)
.downloadOnly(object : SimpleTarget<File>() {
override fun onResourceReady(resource: File, anim: GlideAnimation<in File>) {
try {
// Copy the cover from Glide's cache to local cache.
copyToCache(thumbnailUrl!!, resource)
onReady?.invoke(resource)
} catch (e: IOException) {
// Do nothing.
}
}
})
}
/**
* Save or load the image from cache
* @param thumbnailUrl the thumbnail url.
* @param headers headers included in Glide request.
* @param onReady function to call when the image is ready
*/
fun saveOrLoadFromCache(thumbnailUrl: String?, headers: LazyHeaders, onReady: ((File) -> Unit)?) {
// Check if url is empty.
if (thumbnailUrl.isNullOrEmpty())
return
// If file exist load it otherwise save it.
val localCover = getCoverFromCache(thumbnailUrl!!)
if (localCover.exists()) {
onReady?.invoke(localCover)
} else {
save(thumbnailUrl, headers, onReady)
}
}
/**
* Returns the cover from cache.
*
* @param thumbnailUrl the thumbnail url.
* @return cover image.
*/
private fun getCoverFromCache(thumbnailUrl: String): File {
fun getCoverFile(thumbnailUrl: String): File {
return File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl))
}
/**
* Copy the given file to this cache.
* @param thumbnailUrl url of thumbnail.
* @param sourceFile the source file of the cover image.
* @throws IOException if there's any error.
*/
@Throws(IOException::class)
fun copyToCache(thumbnailUrl: String, sourceFile: File) {
// Get destination file.
val destFile = getCoverFromCache(thumbnailUrl)
sourceFile.copyTo(destFile, overwrite = true)
}
/**
* Copy the given stream to this cache.
*
* @param thumbnailUrl url of the thumbnail.
* @param inputStream the stream to copy.
* @throws IOException if there's any error.
@ -108,13 +42,14 @@ class CoverCache(private val context: Context) {
@Throws(IOException::class)
fun copyToCache(thumbnailUrl: String, inputStream: InputStream) {
// Get destination file.
val destFile = getCoverFromCache(thumbnailUrl)
val destFile = getCoverFile(thumbnailUrl)
destFile.outputStream().use { inputStream.copyTo(it) }
}
/**
* Delete the cover file from the cache.
*
* @param thumbnailUrl the thumbnail url.
* @return status of deletion.
*/
@ -124,7 +59,7 @@ class CoverCache(private val context: Context) {
return false
// Remove file.
val file = File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl))
val file = getCoverFile(thumbnailUrl!!)
return file.exists() && file.delete()
}

View File

@ -1,22 +0,0 @@
package eu.kanade.tachiyomi.data.cache
import android.content.Context
import com.bumptech.glide.Glide
import com.bumptech.glide.GlideBuilder
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
import com.bumptech.glide.module.GlideModule
/**
* Class used to update Glide module settings
*/
class CoverGlideModule : GlideModule {
override fun applyOptions(context: Context, builder: GlideBuilder) {
// Set the cache size of Glide to 15 MiB
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 15 * 1024 * 1024))
}
override fun registerComponents(context: Context, glide: Glide) {
// Nothing to see here!
}
}

View File

@ -1,26 +1,17 @@
package eu.kanade.tachiyomi.data.database
import android.content.Context
import android.util.Pair
import com.pushtorefresh.storio.Queries
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetObject
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.Query
import com.pushtorefresh.storio.sqlite.queries.RawQuery
import eu.kanade.tachiyomi.data.database.models.*
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver
import eu.kanade.tachiyomi.data.database.tables.*
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService
import eu.kanade.tachiyomi.data.source.base.Source
import eu.kanade.tachiyomi.util.ChapterRecognition
import rx.Observable
import java.util.*
import eu.kanade.tachiyomi.data.database.queries.*
open class DatabaseHelper(context: Context) {
/**
* This class provides operations to manage the database through its interfaces.
*/
open class DatabaseHelper(context: Context)
: MangaQueries, ChapterQueries, MangaSyncQueries, CategoryQueries, MangaCategoryQueries {
val db = DefaultStorIOSQLite.builder()
override val db = DefaultStorIOSQLite.builder()
.sqliteOpenHelper(DbOpenHelper(context))
.addTypeMapping(Manga::class.java, MangaSQLiteTypeMapping())
.addTypeMapping(Chapter::class.java, ChapterSQLiteTypeMapping())
@ -29,279 +20,6 @@ open class DatabaseHelper(context: Context) {
.addTypeMapping(MangaCategory::class.java, MangaCategorySQLiteTypeMapping())
.build()
inline fun inTransaction(func: DatabaseHelper.() -> Unit) {
db.internal().beginTransaction()
try {
func()
db.internal().setTransactionSuccessful()
} finally {
db.internal().endTransaction()
}
}
// Mangas related queries
fun getMangas() = db.get()
.listOfObjects(Manga::class.java)
.withQuery(Query.builder()
.table(MangaTable.TABLE)
.build())
.prepare()
fun getLibraryMangas() = db.get()
.listOfObjects(Manga::class.java)
.withQuery(RawQuery.builder()
.query(libraryQuery)
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE)
.build())
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
.prepare()
open fun getFavoriteMangas() = db.get()
.listOfObjects(Manga::class.java)
.withQuery(Query.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COLUMN_FAVORITE} = ?")
.whereArgs(1)
.orderBy(MangaTable.COLUMN_TITLE)
.build())
.prepare()
fun getManga(url: String, sourceId: Int) = db.get()
.`object`(Manga::class.java)
.withQuery(Query.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COLUMN_URL} = ? AND ${MangaTable.COLUMN_SOURCE} = ?")
.whereArgs(url, sourceId)
.build())
.prepare()
fun getManga(id: Long) = db.get()
.`object`(Manga::class.java)
.withQuery(Query.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COLUMN_ID} = ?")
.whereArgs(id)
.build())
.prepare()
fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
fun insertMangas(mangas: List<Manga>) = db.put().objects(mangas).prepare()
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
fun deleteMangasNotInLibrary() = db.delete()
.byQuery(DeleteQuery.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COLUMN_FAVORITE} = ?")
.whereArgs(0)
.build())
.prepare()
// Chapters related queries
fun getChapters(manga: Manga) = db.get()
.listOfObjects(Chapter::class.java)
.withQuery(Query.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COLUMN_MANGA_ID} = ?")
.whereArgs(manga.id)
.build())
.prepare()
fun getRecentChapters(date: Date) = db.get()
.listOfObjects(MangaChapter::class.java)
.withQuery(RawQuery.builder()
.query(getRecentsQuery(date))
.observesTables(ChapterTable.TABLE)
.build())
.withGetResolver(MangaChapterGetResolver.INSTANCE)
.prepare()
fun getNextChapter(chapter: Chapter): PreparedGetObject<Chapter> {
// Add a delta to the chapter number, because binary decimal representation
// can retrieve the same chapter again
val chapterNumber = chapter.chapter_number + 0.00001
return db.get()
.`object`(Chapter::class.java)
.withQuery(Query.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COLUMN_MANGA_ID} = ? AND " +
"${ChapterTable.COLUMN_CHAPTER_NUMBER} > ? AND " +
"${ChapterTable.COLUMN_CHAPTER_NUMBER} <= ?")
.whereArgs(chapter.manga_id, chapterNumber, chapterNumber + 1)
.orderBy(ChapterTable.COLUMN_CHAPTER_NUMBER)
.limit(1)
.build())
.prepare()
}
fun getPreviousChapter(chapter: Chapter): PreparedGetObject<Chapter> {
// Add a delta to the chapter number, because binary decimal representation
// can retrieve the same chapter again
val chapterNumber = chapter.chapter_number - 0.00001
return db.get()
.`object`(Chapter::class.java)
.withQuery(Query.builder().table(ChapterTable.TABLE)
.where("${ChapterTable.COLUMN_MANGA_ID} = ? AND " +
"${ChapterTable.COLUMN_CHAPTER_NUMBER} < ? AND " +
"${ChapterTable.COLUMN_CHAPTER_NUMBER} >= ?")
.whereArgs(chapter.manga_id, chapterNumber, chapterNumber - 1)
.orderBy(ChapterTable.COLUMN_CHAPTER_NUMBER + " DESC")
.limit(1)
.build())
.prepare()
}
fun getNextUnreadChapter(manga: Manga) = db.get()
.`object`(Chapter::class.java)
.withQuery(Query.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COLUMN_MANGA_ID} = ? AND " +
"${ChapterTable.COLUMN_READ} = ? AND " +
"${ChapterTable.COLUMN_CHAPTER_NUMBER} >= ?")
.whereArgs(manga.id, 0, 0)
.orderBy(ChapterTable.COLUMN_CHAPTER_NUMBER)
.limit(1)
.build())
.prepare()
fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare()
fun insertChapters(chapters: List<Chapter>) = db.put().objects(chapters).prepare()
// Add new chapters or delete if the source deletes them
open fun insertOrRemoveChapters(manga: Manga, sourceChapters: List<Chapter>, source: Source): Observable<Pair<Int, Int>> {
val dbChapters = getChapters(manga).executeAsBlocking()
val newChapters = Observable.from(sourceChapters)
.filter { it !in dbChapters }
.doOnNext { c ->
c.manga_id = manga.id
source.parseChapterNumber(c)
ChapterRecognition.parseChapterNumber(c, manga)
}.toList()
val deletedChapters = Observable.from(dbChapters)
.filter { it !in sourceChapters }
.toList()
return Observable.zip(newChapters, deletedChapters) { toAdd, toDelete ->
var added = 0
var deleted = 0
var readded = 0
inTransaction {
val deletedReadChapterNumbers = TreeSet<Float>()
if (!toDelete.isEmpty()) {
for (c in toDelete) {
if (c.read) {
deletedReadChapterNumbers.add(c.chapter_number)
}
}
deleted = deleteChapters(toDelete).executeAsBlocking().results().size
}
if (!toAdd.isEmpty()) {
// Set the date fetch for new items in reverse order to allow another sorting method.
// Sources MUST return the chapters from most to less recent, which is common.
var now = Date().time
for (i in toAdd.indices.reversed()) {
val c = toAdd[i]
c.date_fetch = now++
// Try to mark already read chapters as read when the source deletes them
if (c.chapter_number != -1f && c.chapter_number in deletedReadChapterNumbers) {
c.read = true
readded++
}
}
added = insertChapters(toAdd).executeAsBlocking().numberOfInserts()
}
}
Pair.create(added - readded, deleted - readded)
}
}
fun deleteChapter(chapter: Chapter) = db.delete().`object`(chapter).prepare()
fun deleteChapters(chapters: List<Chapter>) = db.delete().objects(chapters).prepare()
// Manga sync related queries
fun getMangaSync(manga: Manga, sync: MangaSyncService) = db.get()
.`object`(MangaSync::class.java)
.withQuery(Query.builder()
.table(MangaSyncTable.TABLE)
.where("${MangaSyncTable.COLUMN_MANGA_ID} = ? AND " +
"${MangaSyncTable.COLUMN_SYNC_ID} = ?")
.whereArgs(manga.id, sync.id)
.build())
.prepare()
fun getMangasSync(manga: Manga) = db.get()
.listOfObjects(MangaSync::class.java)
.withQuery(Query.builder()
.table(MangaSyncTable.TABLE)
.where("${MangaSyncTable.COLUMN_MANGA_ID} = ?")
.whereArgs(manga.id)
.build())
.prepare()
fun insertMangaSync(manga: MangaSync) = db.put().`object`(manga).prepare()
fun insertMangasSync(mangas: List<MangaSync>) = db.put().objects(mangas).prepare()
fun deleteMangaSync(manga: MangaSync) = db.delete().`object`(manga).prepare()
// Categories related queries
fun getCategories() = db.get()
.listOfObjects(Category::class.java)
.withQuery(Query.builder()
.table(CategoryTable.TABLE)
.orderBy(CategoryTable.COLUMN_ORDER)
.build())
.prepare()
fun getCategoriesForManga(manga: Manga) = db.get()
.listOfObjects(Category::class.java)
.withQuery(RawQuery.builder()
.query(getCategoriesForMangaQuery(manga))
.build())
.prepare()
fun insertCategory(category: Category) = db.put().`object`(category).prepare()
fun insertCategories(categories: List<Category>) = db.put().objects(categories).prepare()
fun deleteCategory(category: Category) = db.delete().`object`(category).prepare()
fun deleteCategories(categories: List<Category>) = db.delete().objects(categories).prepare()
fun insertMangaCategory(mangaCategory: MangaCategory) = db.put().`object`(mangaCategory).prepare()
fun insertMangasCategories(mangasCategories: List<MangaCategory>) = db.put().objects(mangasCategories).prepare()
fun deleteOldMangasCategories(mangas: List<Manga>) = db.delete()
.byQuery(DeleteQuery.builder()
.table(MangaCategoryTable.TABLE)
.where("${MangaCategoryTable.COLUMN_MANGA_ID} IN (${Queries.placeholders(mangas.size)})")
.whereArgs(*mangas.map { it.id }.toTypedArray())
.build())
.prepare()
fun setMangaCategories(mangasCategories: List<MangaCategory>, mangas: List<Manga>) {
inTransaction {
deleteOldMangasCategories(mangas).executeAsBlocking()
insertMangasCategories(mangasCategories).executeAsBlocking()
}
}
inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)
}

View File

@ -0,0 +1,25 @@
package eu.kanade.tachiyomi.data.database
import com.pushtorefresh.storio.sqlite.StorIOSQLite
inline fun StorIOSQLite.inTransaction(block: () -> Unit) {
internal().beginTransaction()
try {
block()
internal().setTransactionSuccessful()
} finally {
internal().endTransaction()
}
}
inline fun <T> StorIOSQLite.inTransactionReturn(block: () -> T): T {
internal().beginTransaction()
try {
val result = block()
internal().setTransactionSuccessful()
return result
} finally {
internal().endTransaction()
}
}

View File

@ -5,7 +5,8 @@ import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import eu.kanade.tachiyomi.data.database.tables.*
class DbOpenHelper(context: Context) : SQLiteOpenHelper(context, DbOpenHelper.DATABASE_NAME, null, DbOpenHelper.DATABASE_VERSION) {
class DbOpenHelper(context: Context)
: SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
companion object {
/**
@ -16,24 +17,30 @@ class DbOpenHelper(context: Context) : SQLiteOpenHelper(context, DbOpenHelper.DA
/**
* Version of the database.
*/
const val DATABASE_VERSION = 1
const val DATABASE_VERSION = 2
}
override fun onCreate(db: SQLiteDatabase) = with(db) {
execSQL(MangaTable.getCreateTableQuery())
execSQL(ChapterTable.getCreateTableQuery())
execSQL(MangaSyncTable.getCreateTableQuery())
execSQL(CategoryTable.getCreateTableQuery())
execSQL(MangaCategoryTable.getCreateTableQuery())
execSQL(MangaTable.createTableQuery)
execSQL(ChapterTable.createTableQuery)
execSQL(MangaSyncTable.createTableQuery)
execSQL(CategoryTable.createTableQuery)
execSQL(MangaCategoryTable.createTableQuery)
// DB indexes
execSQL(MangaTable.getCreateUrlIndexQuery())
execSQL(MangaTable.getCreateFavoriteIndexQuery())
execSQL(ChapterTable.getCreateMangaIdIndexQuery())
execSQL(MangaTable.createUrlIndexQuery)
execSQL(MangaTable.createFavoriteIndexQuery)
execSQL(ChapterTable.createMangaIdIndexQuery)
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
if (oldVersion < 2) {
db.execSQL(ChapterTable.sourceOrderUpdateQuery)
// Fix kissmanga covers after supporting cloudflare
db.execSQL("""UPDATE mangas SET thumbnail_url =
REPLACE(thumbnail_url, '93.174.95.110', 'kissmanga.com') WHERE source = 4""")
}
}
override fun onConfigure(db: SQLiteDatabase) {

View File

@ -0,0 +1,9 @@
package eu.kanade.tachiyomi.data.database
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
interface DbProvider {
val db: DefaultStorIOSQLite
}

View File

@ -1,54 +0,0 @@
package eu.kanade.tachiyomi.data.database
import java.util.*
import eu.kanade.tachiyomi.data.database.models.Manga as MangaModel
import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category
import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable as MangaCategory
import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga
/**
* Query to get the manga from the library, with their categories and unread count.
*/
val libraryQuery =
"SELECT M.*, COALESCE(MC.${MangaCategory.COLUMN_CATEGORY_ID}, 0) AS ${Manga.COLUMN_CATEGORY} " +
"FROM (" +
"SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COLUMN_UNREAD} " +
"FROM ${Manga.TABLE} " +
"LEFT JOIN (" +
"SELECT ${Chapter.COLUMN_MANGA_ID}, COUNT(*) AS unread " +
"FROM ${Chapter.TABLE} " +
"WHERE ${Chapter.COLUMN_READ} = 0 " +
"GROUP BY ${Chapter.COLUMN_MANGA_ID}" +
") AS C " +
"ON ${Manga.COLUMN_ID} = C.${Chapter.COLUMN_MANGA_ID} " +
"WHERE ${Manga.COLUMN_FAVORITE} = 1 " +
"GROUP BY ${Manga.COLUMN_ID} " +
"ORDER BY ${Manga.COLUMN_TITLE}" +
") AS M " +
"LEFT JOIN (" +
"SELECT * FROM ${MangaCategory.TABLE}) AS MC " +
"ON MC.${MangaCategory.COLUMN_MANGA_ID} = M.${Manga.COLUMN_ID}"
/**
* Query to get the recent chapters of manga from the library up to a date.
*
* @param date the delimiting date.
*/
fun getRecentsQuery(date: Date): String =
"SELECT ${Manga.TABLE}.${Manga.COLUMN_URL} as mangaUrl, * FROM ${Manga.TABLE} JOIN ${Chapter.TABLE} " +
"ON ${Manga.TABLE}.${Manga.COLUMN_ID} = ${Chapter.TABLE}.${Chapter.COLUMN_MANGA_ID} " +
"WHERE ${Manga.COLUMN_FAVORITE} = 1 AND ${Chapter.COLUMN_DATE_UPLOAD} > ${date.time} " +
"ORDER BY ${Chapter.COLUMN_DATE_UPLOAD} DESC"
/**
* Query to get the categorias for a manga.
*
* @param manga the manga.
*/
fun getCategoriesForMangaQuery(manga: MangaModel) =
"SELECT ${Category.TABLE}.* FROM ${Category.TABLE} " +
"JOIN ${MangaCategory.TABLE} ON ${Category.TABLE}.${Category.COLUMN_ID} = " +
"${MangaCategory.TABLE}.${MangaCategory.COLUMN_CATEGORY_ID} " +
"WHERE ${MangaCategory.COLUMN_MANGA_ID} = ${manga.id}"

View File

@ -10,16 +10,16 @@ import eu.kanade.tachiyomi.data.database.tables.CategoryTable;
@StorIOSQLiteType(table = CategoryTable.TABLE)
public class Category implements Serializable {
@StorIOSQLiteColumn(name = CategoryTable.COLUMN_ID, key = true)
@StorIOSQLiteColumn(name = CategoryTable.COL_ID, key = true)
public Integer id;
@StorIOSQLiteColumn(name = CategoryTable.COLUMN_NAME)
@StorIOSQLiteColumn(name = CategoryTable.COL_NAME)
public String name;
@StorIOSQLiteColumn(name = CategoryTable.COLUMN_ORDER)
@StorIOSQLiteColumn(name = CategoryTable.COL_ORDER)
public int order;
@StorIOSQLiteColumn(name = CategoryTable.COLUMN_FLAGS)
@StorIOSQLiteColumn(name = CategoryTable.COL_FLAGS)
public int flags;
public Category() {}

View File

@ -14,33 +14,36 @@ import eu.kanade.tachiyomi.util.UrlUtil;
@StorIOSQLiteType(table = ChapterTable.TABLE)
public class Chapter implements Serializable {
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_ID, key = true)
@StorIOSQLiteColumn(name = ChapterTable.COL_ID, key = true)
public Long id;
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_MANGA_ID)
@StorIOSQLiteColumn(name = ChapterTable.COL_MANGA_ID)
public Long manga_id;
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_URL)
@StorIOSQLiteColumn(name = ChapterTable.COL_URL)
public String url;
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_NAME)
@StorIOSQLiteColumn(name = ChapterTable.COL_NAME)
public String name;
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_READ)
@StorIOSQLiteColumn(name = ChapterTable.COL_READ)
public boolean read;
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_LAST_PAGE_READ)
@StorIOSQLiteColumn(name = ChapterTable.COL_LAST_PAGE_READ)
public int last_page_read;
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_DATE_FETCH)
@StorIOSQLiteColumn(name = ChapterTable.COL_DATE_FETCH)
public long date_fetch;
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_DATE_UPLOAD)
@StorIOSQLiteColumn(name = ChapterTable.COL_DATE_UPLOAD)
public long date_upload;
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_CHAPTER_NUMBER)
@StorIOSQLiteColumn(name = ChapterTable.COL_CHAPTER_NUMBER)
public float chapter_number;
@StorIOSQLiteColumn(name = ChapterTable.COL_SOURCE_ORDER)
public int source_order;
public int status;
private transient List<Page> pages;
@ -84,4 +87,8 @@ public class Chapter implements Serializable {
public boolean isDownloaded() {
return status == Download.DOWNLOADED;
}
public boolean isRecognizedNumber() {
return chapter_number >= 0f;
}
}

View File

@ -14,49 +14,49 @@ import eu.kanade.tachiyomi.util.UrlUtil;
@StorIOSQLiteType(table = MangaTable.TABLE)
public class Manga implements Serializable {
@StorIOSQLiteColumn(name = MangaTable.COLUMN_ID, key = true)
@StorIOSQLiteColumn(name = MangaTable.COL_ID, key = true)
public Long id;
@StorIOSQLiteColumn(name = MangaTable.COLUMN_SOURCE)
@StorIOSQLiteColumn(name = MangaTable.COL_SOURCE)
public int source;
@StorIOSQLiteColumn(name = MangaTable.COLUMN_URL)
@StorIOSQLiteColumn(name = MangaTable.COL_URL)
public String url;
@StorIOSQLiteColumn(name = MangaTable.COLUMN_ARTIST)
@StorIOSQLiteColumn(name = MangaTable.COL_ARTIST)
public String artist;
@StorIOSQLiteColumn(name = MangaTable.COLUMN_AUTHOR)
@StorIOSQLiteColumn(name = MangaTable.COL_AUTHOR)
public String author;
@StorIOSQLiteColumn(name = MangaTable.COLUMN_DESCRIPTION)
@StorIOSQLiteColumn(name = MangaTable.COL_DESCRIPTION)
public String description;
@StorIOSQLiteColumn(name = MangaTable.COLUMN_GENRE)
@StorIOSQLiteColumn(name = MangaTable.COL_GENRE)
public String genre;
@StorIOSQLiteColumn(name = MangaTable.COLUMN_TITLE)
@StorIOSQLiteColumn(name = MangaTable.COL_TITLE)
public String title;
@StorIOSQLiteColumn(name = MangaTable.COLUMN_STATUS)
@StorIOSQLiteColumn(name = MangaTable.COL_STATUS)
public int status;
@StorIOSQLiteColumn(name = MangaTable.COLUMN_THUMBNAIL_URL)
@StorIOSQLiteColumn(name = MangaTable.COL_THUMBNAIL_URL)
public String thumbnail_url;
@StorIOSQLiteColumn(name = MangaTable.COLUMN_FAVORITE)
@StorIOSQLiteColumn(name = MangaTable.COL_FAVORITE)
public boolean favorite;
@StorIOSQLiteColumn(name = MangaTable.COLUMN_LAST_UPDATE)
@StorIOSQLiteColumn(name = MangaTable.COL_LAST_UPDATE)
public long last_update;
@StorIOSQLiteColumn(name = MangaTable.COLUMN_INITIALIZED)
@StorIOSQLiteColumn(name = MangaTable.COL_INITIALIZED)
public boolean initialized;
@StorIOSQLiteColumn(name = MangaTable.COLUMN_VIEWER)
@StorIOSQLiteColumn(name = MangaTable.COL_VIEWER)
public int viewer;
@StorIOSQLiteColumn(name = MangaTable.COLUMN_CHAPTER_FLAGS)
@StorIOSQLiteColumn(name = MangaTable.COL_CHAPTER_FLAGS)
public int chapter_flags;
public transient int unread;
@ -68,10 +68,13 @@ public class Manga implements Serializable {
public static final int COMPLETED = 2;
public static final int LICENSED = 3;
public static final int SORT_AZ = 0x00000000;
public static final int SORT_ZA = 0x00000001;
public static final int SORT_DESC = 0x00000000;
public static final int SORT_ASC = 0x00000001;
public static final int SORT_MASK = 0x00000001;
// Generic filter that does not filter anything
public static final int SHOW_ALL = 0x00000000;
public static final int SHOW_UNREAD = 0x00000002;
public static final int SHOW_READ = 0x00000004;
public static final int READ_MASK = 0x00000006;
@ -80,8 +83,9 @@ public class Manga implements Serializable {
public static final int SHOW_NOT_DOWNLOADED = 0x00000010;
public static final int DOWNLOADED_MASK = 0x00000018;
// Generic filter that does not filter anything
public static final int SHOW_ALL = 0x00000000;
public static final int SORTING_SOURCE = 0x00000000;
public static final int SORTING_NUMBER = 0x00000100;
public static final int SORTING_MASK = 0x00000100;
public static final int DISPLAY_NAME = 0x00000000;
public static final int DISPLAY_NUMBER = 0x00100000;
@ -95,6 +99,13 @@ public class Manga implements Serializable {
return m;
}
public static Manga create(String pathUrl, int source) {
Manga m = new Manga();
m.url = pathUrl;
m.source = source;
return m;
}
public void setUrl(String url) {
this.url = UrlUtil.getPath(url);
}
@ -155,12 +166,16 @@ public class Manga implements Serializable {
setFlags(filter, DOWNLOADED_MASK);
}
public void setSorting(int sort) {
setFlags(sort, SORTING_MASK);
}
private void setFlags(int flag, int mask) {
chapter_flags = (chapter_flags & ~mask) | (flag & mask);
}
public boolean sortChaptersAZ() {
return (chapter_flags & SORT_MASK) == SORT_AZ;
public boolean sortDescending() {
return (chapter_flags & SORT_MASK) == SORT_DESC;
}
// Used to display the chapter's title one way or another
@ -176,6 +191,10 @@ public class Manga implements Serializable {
return chapter_flags & DOWNLOADED_MASK;
}
public int getSorting() {
return chapter_flags & SORTING_MASK;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View File

@ -8,13 +8,13 @@ import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable;
@StorIOSQLiteType(table = MangaCategoryTable.TABLE)
public class MangaCategory {
@StorIOSQLiteColumn(name = MangaCategoryTable.COLUMN_ID, key = true)
@StorIOSQLiteColumn(name = MangaCategoryTable.COL_ID, key = true)
public Long id;
@StorIOSQLiteColumn(name = MangaCategoryTable.COLUMN_MANGA_ID)
@StorIOSQLiteColumn(name = MangaCategoryTable.COL_MANGA_ID)
public long manga_id;
@StorIOSQLiteColumn(name = MangaCategoryTable.COLUMN_CATEGORY_ID)
@StorIOSQLiteColumn(name = MangaCategoryTable.COL_CATEGORY_ID)
public int category_id;
public MangaCategory() {}

View File

@ -6,36 +6,36 @@ import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
import java.io.Serializable;
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable;
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService;
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService;
@StorIOSQLiteType(table = MangaSyncTable.TABLE)
public class MangaSync implements Serializable {
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_ID, key = true)
@StorIOSQLiteColumn(name = MangaSyncTable.COL_ID, key = true)
public Long id;
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_MANGA_ID)
@StorIOSQLiteColumn(name = MangaSyncTable.COL_MANGA_ID)
public long manga_id;
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_SYNC_ID)
@StorIOSQLiteColumn(name = MangaSyncTable.COL_SYNC_ID)
public int sync_id;
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_REMOTE_ID)
@StorIOSQLiteColumn(name = MangaSyncTable.COL_REMOTE_ID)
public int remote_id;
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_TITLE)
@StorIOSQLiteColumn(name = MangaSyncTable.COL_TITLE)
public String title;
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_LAST_CHAPTER_READ)
@StorIOSQLiteColumn(name = MangaSyncTable.COL_LAST_CHAPTER_READ)
public int last_chapter_read;
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_TOTAL_CHAPTERS)
@StorIOSQLiteColumn(name = MangaSyncTable.COL_TOTAL_CHAPTERS)
public int total_chapters;
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_SCORE)
@StorIOSQLiteColumn(name = MangaSyncTable.COL_SCORE)
public float score;
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_STATUS)
@StorIOSQLiteColumn(name = MangaSyncTable.COL_STATUS)
public int status;
public boolean update;

View File

@ -0,0 +1,36 @@
package eu.kanade.tachiyomi.data.database.queries
import com.pushtorefresh.storio.sqlite.queries.Query
import com.pushtorefresh.storio.sqlite.queries.RawQuery
import eu.kanade.tachiyomi.data.database.DbProvider
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
interface CategoryQueries : DbProvider {
fun getCategories() = db.get()
.listOfObjects(Category::class.java)
.withQuery(Query.builder()
.table(CategoryTable.TABLE)
.orderBy(CategoryTable.COL_ORDER)
.build())
.prepare()
fun getCategoriesForManga(manga: Manga) = db.get()
.listOfObjects(Category::class.java)
.withQuery(RawQuery.builder()
.query(getCategoriesForMangaQuery())
.args(manga.id)
.build())
.prepare()
fun insertCategory(category: Category) = db.put().`object`(category).prepare()
fun insertCategories(categories: List<Category>) = db.put().objects(categories).prepare()
fun deleteCategory(category: Category) = db.delete().`object`(category).prepare()
fun deleteCategories(categories: List<Category>) = db.delete().objects(categories).prepare()
}

View File

@ -0,0 +1,134 @@
package eu.kanade.tachiyomi.data.database.queries
import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetObject
import com.pushtorefresh.storio.sqlite.queries.Query
import com.pushtorefresh.storio.sqlite.queries.RawQuery
import eu.kanade.tachiyomi.data.database.DbProvider
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaChapter
import eu.kanade.tachiyomi.data.database.resolvers.ChapterProgressPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.ChapterSourceOrderPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
import java.util.*
interface ChapterQueries : DbProvider {
fun getChapters(manga: Manga) = db.get()
.listOfObjects(Chapter::class.java)
.withQuery(Query.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_MANGA_ID} = ?")
.whereArgs(manga.id)
.build())
.prepare()
fun getRecentChapters(date: Date) = db.get()
.listOfObjects(MangaChapter::class.java)
.withQuery(RawQuery.builder()
.query(getRecentsQuery())
.args(date.time)
.observesTables(ChapterTable.TABLE)
.build())
.withGetResolver(MangaChapterGetResolver.INSTANCE)
.prepare()
fun getNextChapter(chapter: Chapter): PreparedGetObject<Chapter> {
// Add a delta to the chapter number, because binary decimal representation
// can retrieve the same chapter again
val chapterNumber = chapter.chapter_number + 0.00001
return db.get()
.`object`(Chapter::class.java)
.withQuery(Query.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_MANGA_ID} = ? AND " +
"${ChapterTable.COL_CHAPTER_NUMBER} > ? AND " +
"${ChapterTable.COL_CHAPTER_NUMBER} <= ?")
.whereArgs(chapter.manga_id, chapterNumber, chapterNumber + 1)
.orderBy(ChapterTable.COL_CHAPTER_NUMBER)
.limit(1)
.build())
.prepare()
}
fun getNextChapterBySource(chapter: Chapter) = db.get()
.`object`(Chapter::class.java)
.withQuery(Query.builder()
.table(ChapterTable.TABLE)
.where("""${ChapterTable.COL_MANGA_ID} = ? AND
${ChapterTable.COL_SOURCE_ORDER} < ?""")
.whereArgs(chapter.manga_id, chapter.source_order)
.orderBy("${ChapterTable.COL_SOURCE_ORDER} DESC")
.limit(1)
.build())
.prepare()
fun getPreviousChapter(chapter: Chapter): PreparedGetObject<Chapter> {
// Add a delta to the chapter number, because binary decimal representation
// can retrieve the same chapter again
val chapterNumber = chapter.chapter_number - 0.00001
return db.get()
.`object`(Chapter::class.java)
.withQuery(Query.builder().table(ChapterTable.TABLE)
.where("${ChapterTable.COL_MANGA_ID} = ? AND " +
"${ChapterTable.COL_CHAPTER_NUMBER} < ? AND " +
"${ChapterTable.COL_CHAPTER_NUMBER} >= ?")
.whereArgs(chapter.manga_id, chapterNumber, chapterNumber - 1)
.orderBy("${ChapterTable.COL_CHAPTER_NUMBER} DESC")
.limit(1)
.build())
.prepare()
}
fun getPreviousChapterBySource(chapter: Chapter) = db.get()
.`object`(Chapter::class.java)
.withQuery(Query.builder()
.table(ChapterTable.TABLE)
.where("""${ChapterTable.COL_MANGA_ID} = ? AND
${ChapterTable.COL_SOURCE_ORDER} > ?""")
.whereArgs(chapter.manga_id, chapter.source_order)
.orderBy(ChapterTable.COL_SOURCE_ORDER)
.limit(1)
.build())
.prepare()
fun getNextUnreadChapter(manga: Manga) = db.get()
.`object`(Chapter::class.java)
.withQuery(Query.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_MANGA_ID} = ? AND " +
"${ChapterTable.COL_READ} = ? AND " +
"${ChapterTable.COL_CHAPTER_NUMBER} >= ?")
.whereArgs(manga.id, 0, 0)
.orderBy(ChapterTable.COL_CHAPTER_NUMBER)
.limit(1)
.build())
.prepare()
fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare()
fun insertChapters(chapters: List<Chapter>) = db.put().objects(chapters).prepare()
fun deleteChapter(chapter: Chapter) = db.delete().`object`(chapter).prepare()
fun deleteChapters(chapters: List<Chapter>) = db.delete().objects(chapters).prepare()
fun updateChapterProgress(chapter: Chapter) = db.put()
.`object`(chapter)
.withPutResolver(ChapterProgressPutResolver())
.prepare()
fun updateChaptersProgress(chapters: List<Chapter>) = db.put()
.objects(chapters)
.withPutResolver(ChapterProgressPutResolver())
.prepare()
fun fixChaptersSourceOrder(chapters: List<Chapter>) = db.put()
.objects(chapters)
.withPutResolver(ChapterSourceOrderPutResolver())
.prepare()
}

View File

@ -0,0 +1,32 @@
package eu.kanade.tachiyomi.data.database.queries
import com.pushtorefresh.storio.Queries
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import eu.kanade.tachiyomi.data.database.DbProvider
import eu.kanade.tachiyomi.data.database.inTransaction
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
interface MangaCategoryQueries : DbProvider {
fun insertMangaCategory(mangaCategory: MangaCategory) = db.put().`object`(mangaCategory).prepare()
fun insertMangasCategories(mangasCategories: List<MangaCategory>) = db.put().objects(mangasCategories).prepare()
fun deleteOldMangasCategories(mangas: List<Manga>) = db.delete()
.byQuery(DeleteQuery.builder()
.table(MangaCategoryTable.TABLE)
.where("${MangaCategoryTable.COL_MANGA_ID} IN (${Queries.placeholders(mangas.size)})")
.whereArgs(*mangas.map { it.id }.toTypedArray())
.build())
.prepare()
fun setMangaCategories(mangasCategories: List<MangaCategory>, mangas: List<Manga>) {
db.inTransaction {
deleteOldMangasCategories(mangas).executeAsBlocking()
insertMangasCategories(mangasCategories).executeAsBlocking()
}
}
}

View File

@ -0,0 +1,81 @@
package eu.kanade.tachiyomi.data.database.queries
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.Query
import com.pushtorefresh.storio.sqlite.queries.RawQuery
import eu.kanade.tachiyomi.data.database.DbProvider
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
import eu.kanade.tachiyomi.data.database.tables.MangaTable
interface MangaQueries : DbProvider {
fun getMangas() = db.get()
.listOfObjects(Manga::class.java)
.withQuery(Query.builder()
.table(MangaTable.TABLE)
.build())
.prepare()
fun getLibraryMangas() = db.get()
.listOfObjects(Manga::class.java)
.withQuery(RawQuery.builder()
.query(libraryQuery)
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE)
.build())
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
.prepare()
open fun getFavoriteMangas() = db.get()
.listOfObjects(Manga::class.java)
.withQuery(Query.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_FAVORITE} = ?")
.whereArgs(1)
.orderBy(MangaTable.COL_TITLE)
.build())
.prepare()
fun getManga(url: String, sourceId: Int) = db.get()
.`object`(Manga::class.java)
.withQuery(Query.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_URL} = ? AND ${MangaTable.COL_SOURCE} = ?")
.whereArgs(url, sourceId)
.build())
.prepare()
fun getManga(id: Long) = db.get()
.`object`(Manga::class.java)
.withQuery(Query.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?")
.whereArgs(id)
.build())
.prepare()
fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
fun insertMangas(mangas: List<Manga>) = db.put().objects(mangas).prepare()
fun updateFlags(manga: Manga) = db.put()
.`object`(manga)
.withPutResolver(MangaFlagsPutResolver())
.prepare()
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
fun deleteMangasNotInLibrary() = db.delete()
.byQuery(DeleteQuery.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_FAVORITE} = ?")
.whereArgs(0)
.build())
.prepare()
}

View File

@ -0,0 +1,46 @@
package eu.kanade.tachiyomi.data.database.queries
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.Query
import eu.kanade.tachiyomi.data.database.DbProvider
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaSync
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
interface MangaSyncQueries : DbProvider {
fun getMangaSync(manga: Manga, sync: MangaSyncService) = db.get()
.`object`(MangaSync::class.java)
.withQuery(Query.builder()
.table(MangaSyncTable.TABLE)
.where("${MangaSyncTable.COL_MANGA_ID} = ? AND " +
"${MangaSyncTable.COL_SYNC_ID} = ?")
.whereArgs(manga.id, sync.id)
.build())
.prepare()
fun getMangasSync(manga: Manga) = db.get()
.listOfObjects(MangaSync::class.java)
.withQuery(Query.builder()
.table(MangaSyncTable.TABLE)
.where("${MangaSyncTable.COL_MANGA_ID} = ?")
.whereArgs(manga.id)
.build())
.prepare()
fun insertMangaSync(manga: MangaSync) = db.put().`object`(manga).prepare()
fun insertMangasSync(mangas: List<MangaSync>) = db.put().objects(mangas).prepare()
fun deleteMangaSync(manga: MangaSync) = db.delete().`object`(manga).prepare()
fun deleteMangaSyncForManga(manga: Manga) = db.delete()
.byQuery(DeleteQuery.builder()
.table(MangaSyncTable.TABLE)
.where("${MangaSyncTable.COL_MANGA_ID} = ?")
.whereArgs(manga.id)
.build())
.prepare()
}

View File

@ -0,0 +1,51 @@
package eu.kanade.tachiyomi.data.database.queries
import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category
import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable as MangaCategory
import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga
/**
* Query to get the manga from the library, with their categories and unread count.
*/
val libraryQuery = """
SELECT M.*, COALESCE(MC.${MangaCategory.COL_CATEGORY_ID}, 0) AS ${Manga.COL_CATEGORY}
FROM (
SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD}
FROM ${Manga.TABLE}
LEFT JOIN (
SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS unread
FROM ${Chapter.TABLE}
WHERE ${Chapter.COL_READ} = 0
GROUP BY ${Chapter.COL_MANGA_ID}
) AS C
ON ${Manga.COL_ID} = C.${Chapter.COL_MANGA_ID}
WHERE ${Manga.COL_FAVORITE} = 1
GROUP BY ${Manga.COL_ID}
ORDER BY ${Manga.COL_TITLE}
) AS M
LEFT JOIN (
SELECT * FROM ${MangaCategory.TABLE}) AS MC
ON MC.${MangaCategory.COL_MANGA_ID} = M.${Manga.COL_ID}
"""
/**
* Query to get the recent chapters of manga from the library up to a date.
*/
fun getRecentsQuery() = """
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, * FROM ${Manga.TABLE} JOIN ${Chapter.TABLE}
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
WHERE ${Manga.COL_FAVORITE} = 1 AND ${Chapter.COL_DATE_UPLOAD} > ?
ORDER BY ${Chapter.COL_DATE_UPLOAD} DESC
"""
/**
* Query to get the categories for a manga.
*/
fun getCategoriesForMangaQuery() = """
SELECT ${Category.TABLE}.* FROM ${Category.TABLE}
JOIN ${MangaCategory.TABLE} ON ${Category.TABLE}.${Category.COL_ID} =
${MangaCategory.TABLE}.${MangaCategory.COL_CATEGORY_ID}
WHERE ${MangaCategory.COL_MANGA_ID} = ?
"""

View File

@ -0,0 +1,34 @@
package eu.kanade.tachiyomi.data.database.resolvers
import android.content.ContentValues
import com.pushtorefresh.storio.sqlite.StorIOSQLite
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.inTransactionReturn
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
class ChapterProgressPutResolver : PutResolver<Chapter>() {
override fun performPut(db: StorIOSQLite, chapter: Chapter) = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(chapter)
val contentValues = mapToContentValues(chapter)
val numberOfRowsUpdated = db.internal().update(updateQuery, contentValues)
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
}
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_ID} = ?")
.whereArgs(chapter.id)
.build()
fun mapToContentValues(chapter: Chapter) = ContentValues(2).apply {
put(ChapterTable.COL_READ, chapter.read)
put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read)
}
}

View File

@ -0,0 +1,32 @@
package eu.kanade.tachiyomi.data.database.resolvers
import android.content.ContentValues
import com.pushtorefresh.storio.sqlite.StorIOSQLite
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.inTransactionReturn
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
class ChapterSourceOrderPutResolver : PutResolver<Chapter>() {
override fun performPut(db: StorIOSQLite, chapter: Chapter) = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(chapter)
val contentValues = mapToContentValues(chapter)
val numberOfRowsUpdated = db.internal().update(updateQuery, contentValues)
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
}
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?")
.whereArgs(chapter.url, chapter.manga_id)
.build()
fun mapToContentValues(chapter: Chapter) = ContentValues(1).apply {
put(ChapterTable.COL_SOURCE_ORDER, chapter.source_order)
}
}

View File

@ -15,10 +15,10 @@ class LibraryMangaGetResolver : MangaStorIOSQLiteGetResolver() {
override fun mapFromCursor(cursor: Cursor): Manga {
val manga = super.mapFromCursor(cursor)
val unreadColumn = cursor.getColumnIndex(MangaTable.COLUMN_UNREAD)
val unreadColumn = cursor.getColumnIndex(MangaTable.COL_UNREAD)
manga.unread = cursor.getInt(unreadColumn)
val categoryColumn = cursor.getColumnIndex(MangaTable.COLUMN_CATEGORY)
val categoryColumn = cursor.getColumnIndex(MangaTable.COL_CATEGORY)
manga.category = cursor.getInt(categoryColumn)
return manga

View File

@ -0,0 +1,33 @@
package eu.kanade.tachiyomi.data.database.resolvers
import android.content.ContentValues
import com.pushtorefresh.storio.sqlite.StorIOSQLite
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.inTransactionReturn
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.tables.MangaTable
class MangaFlagsPutResolver : PutResolver<Manga>() {
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(manga)
val contentValues = mapToContentValues(manga)
val numberOfRowsUpdated = db.internal().update(updateQuery, contentValues)
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
}
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?")
.whereArgs(manga.id)
.build()
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
put(MangaTable.COL_CHAPTER_FLAGS, manga.chapter_flags)
}
}

View File

@ -1,39 +0,0 @@
package eu.kanade.tachiyomi.data.database.tables;
import android.support.annotation.NonNull;
public class CategoryTable {
@NonNull
public static final String TABLE = "categories";
@NonNull
public static final String COLUMN_ID = "_id";
@NonNull
public static final String COLUMN_NAME = "name";
@NonNull
public static final String COLUMN_ORDER = "sort";
@NonNull
public static final String COLUMN_FLAGS = "flags";
// This is just class with Meta Data, we don't need instances
private CategoryTable() {
throw new IllegalStateException("No instances please");
}
// Better than static final field -> allows VM to unload useless String
// Because you need this string only once per application life on the device
@NonNull
public static String getCreateTableQuery() {
return "CREATE TABLE " + TABLE + "("
+ COLUMN_ID + " INTEGER NOT NULL PRIMARY KEY, "
+ COLUMN_NAME + " TEXT NOT NULL, "
+ COLUMN_ORDER + " INTEGER NOT NULL, "
+ COLUMN_FLAGS + " INTEGER NOT NULL"
+ ");";
}
}

View File

@ -0,0 +1,23 @@
package eu.kanade.tachiyomi.data.database.tables
object CategoryTable {
const val TABLE = "categories"
const val COL_ID = "_id"
const val COL_NAME = "name"
const val COL_ORDER = "sort"
const val COL_FLAGS = "flags"
val createTableQuery: String
get() = """CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_NAME TEXT NOT NULL,
$COL_ORDER INTEGER NOT NULL,
$COL_FLAGS INTEGER NOT NULL
)"""
}

View File

@ -1,59 +0,0 @@
package eu.kanade.tachiyomi.data.database.tables;
import android.support.annotation.NonNull;
public class ChapterTable {
@NonNull
public static final String TABLE = "chapters";
@NonNull
public static final String COLUMN_ID = "_id";
@NonNull
public static final String COLUMN_MANGA_ID = "manga_id";
@NonNull
public static final String COLUMN_URL = "url";
@NonNull
public static final String COLUMN_NAME = "name";
@NonNull
public static final String COLUMN_READ = "read";
@NonNull
public static final String COLUMN_DATE_FETCH = "date_fetch";
@NonNull
public static final String COLUMN_DATE_UPLOAD = "date_upload";
@NonNull
public static final String COLUMN_LAST_PAGE_READ = "last_page_read";
@NonNull
public static final String COLUMN_CHAPTER_NUMBER = "chapter_number";
@NonNull
public static String getCreateTableQuery() {
return "CREATE TABLE " + TABLE + "("
+ COLUMN_ID + " INTEGER NOT NULL PRIMARY KEY, "
+ COLUMN_MANGA_ID + " INTEGER NOT NULL, "
+ COLUMN_URL + " TEXT NOT NULL, "
+ COLUMN_NAME + " TEXT NOT NULL, "
+ COLUMN_READ + " BOOLEAN NOT NULL, "
+ COLUMN_LAST_PAGE_READ + " INT NOT NULL, "
+ COLUMN_CHAPTER_NUMBER + " FLOAT NOT NULL, "
+ COLUMN_DATE_FETCH + " LONG NOT NULL, "
+ COLUMN_DATE_UPLOAD + " LONG NOT NULL, "
+ "FOREIGN KEY(" + COLUMN_MANGA_ID + ") REFERENCES " + MangaTable.TABLE + "(" + MangaTable.COLUMN_ID + ") "
+ "ON DELETE CASCADE"
+ ");";
}
public static String getCreateMangaIdIndexQuery() {
return "CREATE INDEX " + TABLE + "_" + COLUMN_MANGA_ID + "_index ON " + TABLE + "(" + COLUMN_MANGA_ID + ");";
}
}

View File

@ -0,0 +1,49 @@
package eu.kanade.tachiyomi.data.database.tables
object ChapterTable {
const val TABLE = "chapters"
const val COL_ID = "_id"
const val COL_MANGA_ID = "manga_id"
const val COL_URL = "url"
const val COL_NAME = "name"
const val COL_READ = "read"
const val COL_DATE_FETCH = "date_fetch"
const val COL_DATE_UPLOAD = "date_upload"
const val COL_LAST_PAGE_READ = "last_page_read"
const val COL_CHAPTER_NUMBER = "chapter_number"
const val COL_SOURCE_ORDER = "source_order"
val createTableQuery: String
get() = """CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_MANGA_ID INTEGER NOT NULL,
$COL_URL TEXT NOT NULL,
$COL_NAME TEXT NOT NULL,
$COL_READ BOOLEAN NOT NULL,
$COL_LAST_PAGE_READ INT NOT NULL,
$COL_CHAPTER_NUMBER FLOAT NOT NULL,
$COL_SOURCE_ORDER INTEGER NOT NULL,
$COL_DATE_FETCH LONG NOT NULL,
$COL_DATE_UPLOAD LONG NOT NULL,
FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID})
ON DELETE CASCADE
)"""
val createMangaIdIndexQuery: String
get() = "CREATE INDEX ${TABLE}_${COL_MANGA_ID}_index ON $TABLE($COL_MANGA_ID)"
val sourceOrderUpdateQuery: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SOURCE_ORDER INTEGER DEFAULT 0"
}

View File

@ -1,40 +0,0 @@
package eu.kanade.tachiyomi.data.database.tables;
import android.support.annotation.NonNull;
public class MangaCategoryTable {
@NonNull
public static final String TABLE = "mangas_categories";
@NonNull
public static final String COLUMN_ID = "_id";
@NonNull
public static final String COLUMN_MANGA_ID = "manga_id";
@NonNull
public static final String COLUMN_CATEGORY_ID = "category_id";
// This is just class with Meta Data, we don't need instances
private MangaCategoryTable() {
throw new IllegalStateException("No instances please");
}
// Better than static final field -> allows VM to unload useless String
// Because you need this string only once per application life on the device
@NonNull
public static String getCreateTableQuery() {
return "CREATE TABLE " + TABLE + "("
+ COLUMN_ID + " INTEGER NOT NULL PRIMARY KEY, "
+ COLUMN_MANGA_ID + " INTEGER NOT NULL, "
+ COLUMN_CATEGORY_ID + " INTEGER NOT NULL, "
+ "FOREIGN KEY(" + COLUMN_CATEGORY_ID + ") REFERENCES " + CategoryTable.TABLE + "(" + CategoryTable.COLUMN_ID + ") "
+ "ON DELETE CASCADE, "
+ "FOREIGN KEY(" + COLUMN_MANGA_ID + ") REFERENCES " + MangaTable.TABLE + "(" + MangaTable.COLUMN_ID + ") "
+ "ON DELETE CASCADE"
+ ");";
}
}

View File

@ -0,0 +1,24 @@
package eu.kanade.tachiyomi.data.database.tables
object MangaCategoryTable {
const val TABLE = "mangas_categories"
const val COL_ID = "_id"
const val COL_MANGA_ID = "manga_id"
const val COL_CATEGORY_ID = "category_id"
val createTableQuery: String
get() = """CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_MANGA_ID INTEGER NOT NULL,
$COL_CATEGORY_ID INTEGER NOT NULL,
FOREIGN KEY($COL_CATEGORY_ID) REFERENCES ${CategoryTable.TABLE} (${CategoryTable.COL_ID})
ON DELETE CASCADE,
FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID})
ON DELETE CASCADE
)"""
}

View File

@ -1,45 +0,0 @@
package eu.kanade.tachiyomi.data.database.tables;
import android.support.annotation.NonNull;
public class MangaSyncTable {
public static final String TABLE = "manga_sync";
public static final String COLUMN_ID = "_id";
public static final String COLUMN_MANGA_ID = "manga_id";
public static final String COLUMN_SYNC_ID = "sync_id";
public static final String COLUMN_REMOTE_ID = "remote_id";
public static final String COLUMN_TITLE = "title";
public static final String COLUMN_LAST_CHAPTER_READ = "last_chapter_read";
public static final String COLUMN_STATUS = "status";
public static final String COLUMN_SCORE = "score";
public static final String COLUMN_TOTAL_CHAPTERS = "total_chapters";
@NonNull
public static String getCreateTableQuery() {
return "CREATE TABLE " + TABLE + "("
+ COLUMN_ID + " INTEGER NOT NULL PRIMARY KEY, "
+ COLUMN_MANGA_ID + " INTEGER NOT NULL, "
+ COLUMN_SYNC_ID + " INTEGER NOT NULL, "
+ COLUMN_REMOTE_ID + " INTEGER NOT NULL, "
+ COLUMN_TITLE + " TEXT NOT NULL, "
+ COLUMN_LAST_CHAPTER_READ + " INTEGER NOT NULL, "
+ COLUMN_TOTAL_CHAPTERS + " INTEGER NOT NULL, "
+ COLUMN_STATUS + " INTEGER NOT NULL, "
+ COLUMN_SCORE + " FLOAT NOT NULL, "
+ "UNIQUE (" + COLUMN_MANGA_ID + ", " + COLUMN_SYNC_ID + ") ON CONFLICT REPLACE, "
+ "FOREIGN KEY(" + COLUMN_MANGA_ID + ") REFERENCES " + MangaTable.TABLE + "(" + MangaTable.COLUMN_ID + ") "
+ "ON DELETE CASCADE"
+ ");";
}
}

View File

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.data.database.tables
object MangaSyncTable {
const val TABLE = "manga_sync"
const val COL_ID = "_id"
const val COL_MANGA_ID = "manga_id"
const val COL_SYNC_ID = "sync_id"
const val COL_REMOTE_ID = "remote_id"
const val COL_TITLE = "title"
const val COL_LAST_CHAPTER_READ = "last_chapter_read"
const val COL_STATUS = "status"
const val COL_SCORE = "score"
const val COL_TOTAL_CHAPTERS = "total_chapters"
val createTableQuery: String
get() = """CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_MANGA_ID INTEGER NOT NULL,
$COL_SYNC_ID INTEGER NOT NULL,
$COL_REMOTE_ID INTEGER NOT NULL,
$COL_TITLE TEXT NOT NULL,
$COL_LAST_CHAPTER_READ INTEGER NOT NULL,
$COL_TOTAL_CHAPTERS INTEGER NOT NULL,
$COL_STATUS INTEGER NOT NULL,
$COL_SCORE FLOAT NOT NULL,
UNIQUE ($COL_MANGA_ID, $COL_SYNC_ID) ON CONFLICT REPLACE,
FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID})
ON DELETE CASCADE
)"""
}

View File

@ -1,98 +0,0 @@
package eu.kanade.tachiyomi.data.database.tables;
import android.support.annotation.NonNull;
public class MangaTable {
@NonNull
public static final String TABLE = "mangas";
@NonNull
public static final String COLUMN_ID = "_id";
@NonNull
public static final String COLUMN_SOURCE = "source";
@NonNull
public static final String COLUMN_URL = "url";
@NonNull
public static final String COLUMN_ARTIST = "artist";
@NonNull
public static final String COLUMN_AUTHOR = "author" ;
@NonNull
public static final String COLUMN_DESCRIPTION = "description";
@NonNull
public static final String COLUMN_GENRE = "genre";
@NonNull
public static final String COLUMN_TITLE = "title";
@NonNull
public static final String COLUMN_STATUS = "status";
@NonNull
public static final String COLUMN_THUMBNAIL_URL = "thumbnail_url";
@NonNull
public static final String COLUMN_FAVORITE = "favorite";
@NonNull
public static final String COLUMN_LAST_UPDATE = "last_update";
@NonNull
public static final String COLUMN_INITIALIZED = "initialized";
@NonNull
public static final String COLUMN_VIEWER = "viewer";
@NonNull
public static final String COLUMN_CHAPTER_FLAGS = "chapter_flags";
@NonNull
public static final String COLUMN_UNREAD = "unread";
@NonNull
public static final String COLUMN_CATEGORY = "category";
// This is just class with Meta Data, we don't need instances
private MangaTable() {
throw new IllegalStateException("No instances please");
}
// Better than static final field -> allows VM to unload useless String
// Because you need this string only once per application life on the device
@NonNull
public static String getCreateTableQuery() {
return "CREATE TABLE " + TABLE + "("
+ COLUMN_ID + " INTEGER NOT NULL PRIMARY KEY, "
+ COLUMN_SOURCE + " INTEGER NOT NULL, "
+ COLUMN_URL + " TEXT NOT NULL, "
+ COLUMN_ARTIST + " TEXT, "
+ COLUMN_AUTHOR + " TEXT, "
+ COLUMN_DESCRIPTION + " TEXT, "
+ COLUMN_GENRE + " TEXT, "
+ COLUMN_TITLE + " TEXT NOT NULL, "
+ COLUMN_STATUS + " INTEGER NOT NULL, "
+ COLUMN_THUMBNAIL_URL + " TEXT, "
+ COLUMN_FAVORITE + " INTEGER NOT NULL, "
+ COLUMN_LAST_UPDATE + " LONG, "
+ COLUMN_INITIALIZED + " BOOLEAN NOT NULL, "
+ COLUMN_VIEWER + " INTEGER NOT NULL, "
+ COLUMN_CHAPTER_FLAGS + " INTEGER NOT NULL"
+ ");";
}
public static String getCreateUrlIndexQuery() {
return "CREATE INDEX " + TABLE + "_" + COLUMN_URL + "_index ON " + TABLE + "(" + COLUMN_URL + ");";
}
public static String getCreateFavoriteIndexQuery() {
return "CREATE INDEX " + TABLE + "_" + COLUMN_FAVORITE + "_index ON " + TABLE + "(" + COLUMN_FAVORITE + ");";
}
}

View File

@ -0,0 +1,65 @@
package eu.kanade.tachiyomi.data.database.tables
object MangaTable {
const val TABLE = "mangas"
const val COL_ID = "_id"
const val COL_SOURCE = "source"
const val COL_URL = "url"
const val COL_ARTIST = "artist"
const val COL_AUTHOR = "author"
const val COL_DESCRIPTION = "description"
const val COL_GENRE = "genre"
const val COL_TITLE = "title"
const val COL_STATUS = "status"
const val COL_THUMBNAIL_URL = "thumbnail_url"
const val COL_FAVORITE = "favorite"
const val COL_LAST_UPDATE = "last_update"
const val COL_INITIALIZED = "initialized"
const val COL_VIEWER = "viewer"
const val COL_CHAPTER_FLAGS = "chapter_flags"
const val COL_UNREAD = "unread"
const val COL_CATEGORY = "category"
val createTableQuery: String
get() = """CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_SOURCE INTEGER NOT NULL,
$COL_URL TEXT NOT NULL,
$COL_ARTIST TEXT,
$COL_AUTHOR TEXT,
$COL_DESCRIPTION TEXT,
$COL_GENRE TEXT,
$COL_TITLE TEXT NOT NULL,
$COL_STATUS INTEGER NOT NULL,
$COL_THUMBNAIL_URL TEXT,
$COL_FAVORITE INTEGER NOT NULL,
$COL_LAST_UPDATE LONG,
$COL_INITIALIZED BOOLEAN NOT NULL,
$COL_VIEWER INTEGER NOT NULL,
$COL_CHAPTER_FLAGS INTEGER NOT NULL
)"""
val createUrlIndexQuery: String
get() = "CREATE INDEX ${TABLE}_${COL_URL}_index ON $TABLE($COL_URL)"
val createFavoriteIndexQuery: String
get() = "CREATE INDEX ${TABLE}_${COL_FAVORITE}_index ON $TABLE($COL_FAVORITE)"
}

View File

@ -5,19 +5,21 @@ import android.net.Uri
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.source.Source
import eu.kanade.tachiyomi.data.source.SourceManager
import eu.kanade.tachiyomi.data.source.base.Source
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.event.DownloadChaptersEvent
import eu.kanade.tachiyomi.data.source.online.OnlineSource
import eu.kanade.tachiyomi.util.DiskUtils
import eu.kanade.tachiyomi.util.DynamicConcurrentMergeOperator
import eu.kanade.tachiyomi.util.UrlUtil
import eu.kanade.tachiyomi.util.toast
import eu.kanade.tachiyomi.util.saveImageTo
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
@ -26,10 +28,9 @@ import rx.subjects.BehaviorSubject
import rx.subjects.PublishSubject
import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
import java.io.FileReader
import java.io.IOException
import java.util.*
import java.util.concurrent.TimeUnit
class DownloadManager(private val context: Context, private val sourceManager: SourceManager, private val preferences: PreferencesHelper) {
@ -39,6 +40,8 @@ class DownloadManager(private val context: Context, private val sourceManager: S
val runningSubject = BehaviorSubject.create<Boolean>()
private var downloadsSubscription: Subscription? = null
val downloadNotifier by lazy { DownloadNotifier(context) }
private val threadsSubject = BehaviorSubject.create<Int>()
private var threadsSubscription: Subscription? = null
@ -48,27 +51,37 @@ class DownloadManager(private val context: Context, private val sourceManager: S
val PAGE_LIST_FILE = "index.json"
@Volatile private var isRunning: Boolean = false
@Volatile var isRunning: Boolean = false
private set
private fun initializeSubscriptions() {
downloadsSubscription?.unsubscribe()
threadsSubscription = preferences.downloadThreads().asObservable()
.subscribe { threadsSubject.onNext(it) }
.subscribe {
threadsSubject.onNext(it)
downloadNotifier.multipleDownloadThreads = it > 1
}
downloadsSubscription = downloadsQueueSubject.flatMap { Observable.from(it) }
.lift(DynamicConcurrentMergeOperator<Download, Download>({ downloadChapter(it) }, threadsSubject))
.onBackpressureBuffer()
.observeOn(AndroidSchedulers.mainThread())
.map { download -> areAllDownloadsFinished() }
.subscribe({ finished ->
if (finished!!) {
.subscribe({
// Delete successful downloads from queue
if (it.status == Download.DOWNLOADED) {
// remove downloaded chapter from queue
queue.del(it)
downloadNotifier.onProgressChange(queue)
}
if (areAllDownloadsFinished()) {
DownloadService.stop(context)
}
}, { e ->
DownloadService.stop(context)
Timber.e(e, e.message)
context.toast(e.message)
downloadNotifier.onError(e.message)
})
if (!isRunning) {
@ -94,16 +107,18 @@ class DownloadManager(private val context: Context, private val sourceManager: S
}
// Create a download object for every chapter in the event and add them to the downloads queue
fun onDownloadChaptersEvent(event: DownloadChaptersEvent) {
val manga = event.manga
val source = sourceManager.get(manga.source)
// Create a download object for every chapter and add them to the downloads queue
fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
val source = sourceManager.get(manga.source) as? OnlineSource ?: return
// Add chapters to queue from the start
val sortedChapters = chapters.sortedByDescending { it.source_order }
// Used to avoid downloading chapters with the same name
val addedChapters = ArrayList<String>()
val pending = ArrayList<Download>()
for (chapter in event.chapters) {
for (chapter in sortedChapters) {
if (addedChapters.contains(chapter.name))
continue
@ -115,6 +130,12 @@ class DownloadManager(private val context: Context, private val sourceManager: S
pending.add(download)
}
}
// Initialize queue size
downloadNotifier.initialQueueSize = queue.size
// Show notification
downloadNotifier.onProgressChange(queue)
if (isRunning) downloadsQueueSubject.onNext(pending)
}
@ -166,33 +187,39 @@ class DownloadManager(private val context: Context, private val sourceManager: S
val pageListObservable = if (download.pages == null)
// Pull page list from network and add them to download object
download.source.pullPageListFromNetwork(download.chapter.url)
download.source.fetchPageListFromNetwork(download.chapter)
.doOnNext { pages ->
download.pages = pages
savePageList(download)
}
else
// Or if the page list already exists, start from the file
// Or if the page list already exists, start from the file
Observable.just(download.pages)
return Observable.defer<Download> { pageListObservable
.doOnNext { pages ->
download.downloadedImages = 0
download.status = Download.DOWNLOADING
}
// Get all the URLs to the source images, fetch pages if necessary
.flatMap { download.source.getAllImageUrlsFromPageList(it) }
// Start downloading images, consider we can have downloaded images already
.concatMap { page -> getOrDownloadImage(page, download) }
// Do after download completes
.doOnCompleted { onDownloadCompleted(download) }
.toList()
.map { pages -> download }
// If the page list threw, it will resume here
.onErrorResumeNext { error ->
download.status = Download.ERROR
Observable.just(download)
}
return Observable.defer {
pageListObservable
.doOnNext { pages ->
download.downloadedImages = 0
download.status = Download.DOWNLOADING
}
// Get all the URLs to the source images, fetch pages if necessary
.flatMap { download.source.fetchAllImageUrlsFromPageList(it) }
// Start downloading images, consider we can have downloaded images already
.concatMap { page -> getOrDownloadImage(page, download) }
// Do when page is downloaded.
.doOnNext {
downloadNotifier.onProgressChange(download, queue)
}
// Do after download completes
.doOnCompleted { onDownloadCompleted(download) }
.toList()
.map { pages -> download }
// If the page list threw, it will resume here
.onErrorResumeNext { error ->
download.status = Download.ERROR
downloadNotifier.onError(error.message, download.chapter.name)
Observable.just(download)
}
}.subscribeOn(Schedulers.io())
}
@ -228,13 +255,24 @@ class DownloadManager(private val context: Context, private val sourceManager: S
}
// Save image on disk
private fun downloadImage(page: Page, source: Source, directory: File, filename: String): Observable<Page> {
private fun downloadImage(page: Page, source: OnlineSource, directory: File, filename: String): Observable<Page> {
page.status = Page.DOWNLOAD_IMAGE
return source.getImageProgressResponse(page)
.flatMap({ resp ->
DiskUtils.saveBufferedSourceToDirectory(resp.body().source(), directory, filename)
return source.imageResponse(page)
.flatMap {
try {
val file = File(directory, filename)
file.parentFile.mkdirs()
it.body().source().saveImageTo(file.outputStream(), preferences.reencodeImage())
} catch (e: Exception) {
it.body().close()
throw e
}
Observable.just(page)
}).retry(2)
}
.retryWhen {
it.zipWith(Observable.range(1, 3)) { errors, retries -> retries }
.flatMap { retries -> Observable.timer((retries * 2).toLong(), TimeUnit.SECONDS) }
}
}
// Public method to get the image from the filesystem. It does NOT provide any way to download the image
@ -287,18 +325,18 @@ class DownloadManager(private val context: Context, private val sourceManager: S
// If any page has an error, the download result will be error
for (page in download.pages) {
actualProgress += page.progress
if (page.status != Page.READY) status = Download.ERROR
if (page.status != Page.READY) {
status = Download.ERROR
downloadNotifier.onError(context.getString(R.string.download_notifier_page_ready_error), download.chapter.name)
}
}
// Ensure that the chapter folder has all the images
if (!isChapterDownloaded(download.directory, download.pages)) {
status = Download.ERROR
downloadNotifier.onError(context.getString(R.string.download_notifier_page_error), download.chapter.name)
}
download.totalProgress = actualProgress
download.status = status
// Delete successful downloads from queue after notifying
if (status == Download.DOWNLOADED) {
queue.del(download)
}
}
// Return the page list from the chapter's directory if it exists, null otherwise
@ -306,26 +344,14 @@ class DownloadManager(private val context: Context, private val sourceManager: S
val chapterDir = getAbsoluteChapterDirectory(source, manga, chapter)
val pagesFile = File(chapterDir, PAGE_LIST_FILE)
var reader: JsonReader? = null
try {
if (pagesFile.exists()) {
reader = JsonReader(FileReader(pagesFile.absolutePath))
val collectionType = object : TypeToken<List<Page>>() {
}.type
return gson.fromJson<List<Page>>(reader, collectionType)
return try {
JsonReader(FileReader(pagesFile)).use {
val collectionType = object : TypeToken<List<Page>>() {}.type
gson.fromJson(it, collectionType)
}
} catch (e: Exception) {
Timber.e(e.cause, e.message)
} finally {
if (reader != null) try {
reader.close()
} catch (e: IOException) {
/* Do nothing */
}
null
}
return null
}
// Shortcut for the method above
@ -338,20 +364,13 @@ class DownloadManager(private val context: Context, private val sourceManager: S
val chapterDir = getAbsoluteChapterDirectory(source, manga, chapter)
val pagesFile = File(chapterDir, PAGE_LIST_FILE)
var out: FileOutputStream? = null
try {
out = FileOutputStream(pagesFile)
out.write(gson.toJson(pages).toByteArray())
out.flush()
} catch (e: IOException) {
Timber.e(e.cause, e.message)
} finally {
if (out != null) try {
out.close()
} catch (e: IOException) {
/* Do nothing */
pagesFile.outputStream().use {
try {
it.write(gson.toJson(pages).toByteArray())
it.flush()
} catch (e: Exception) {
Timber.e(e, e.message)
}
}
}
@ -361,11 +380,11 @@ class DownloadManager(private val context: Context, private val sourceManager: S
}
fun getAbsoluteMangaDirectory(source: Source, manga: Manga): File {
val mangaRelativePath = source.visibleName +
val mangaRelativePath = source.toString() +
File.separator +
manga.title.replace("[^\\sa-zA-Z0-9.-]".toRegex(), "_")
return File(preferences.downloadsDirectory, mangaRelativePath)
return File(preferences.downloadsDirectory().getOrDefault(), mangaRelativePath)
}
// Get the absolute path to the chapter directory
@ -412,13 +431,19 @@ class DownloadManager(private val context: Context, private val sourceManager: S
return !pending.isEmpty()
}
fun stopDownloads() {
fun stopDownloads(errorMessage: String? = null) {
destroySubscriptions()
for (download in queue) {
if (download.status == Download.DOWNLOADING) {
download.status = Download.ERROR
}
}
errorMessage?.let { downloadNotifier.onError(it) }
}
fun clearQueue() {
queue.clear()
downloadNotifier.onClear()
}
}

View File

@ -0,0 +1,166 @@
package eu.kanade.tachiyomi.data.download
import android.content.Context
import android.support.v4.app.NotificationCompat
import eu.kanade.tachiyomi.Constants
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
import eu.kanade.tachiyomi.util.notificationManager
/**
* DownloadNotifier is used to show notifications when downloading one or multiple chapters.
*
* @param context context of application
*/
class DownloadNotifier(private val context: Context) {
/**
* Notification builder.
*/
private val notificationBuilder = NotificationCompat.Builder(context)
/**
* Id of the notification.
*/
private val notificationId: Int
get() = Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ID
/**
* Status of download. Used for correct notification icon.
*/
private var isDownloading = false
/**
* The size of queue on start download.
*/
internal var initialQueueSize = 0
/**
* Simultaneous download setting > 1.
*/
internal var multipleDownloadThreads = false
/**
* Called when download progress changes.
* Note: Only accepted when multi download active.
*
* @param queue the queue containing downloads.
*/
internal fun onProgressChange(queue: DownloadQueue) {
if (multipleDownloadThreads) {
doOnProgressChange(null, queue)
}
}
/**
* Called when download progress changes
* Note: Only accepted when single download active
*
* @param download download object containing download information
* @param queue the queue containing downloads
*/
internal fun onProgressChange(download: Download, queue: DownloadQueue) {
if (!multipleDownloadThreads) {
doOnProgressChange(download, queue)
}
}
/**
* Show notification progress of chapter
*
* @param download download object containing download information
* @param queue the queue containing downloads
*/
private fun doOnProgressChange(download: Download?, queue: DownloadQueue) {
// Check if download is completed
if (multipleDownloadThreads) {
if (queue.isEmpty()) {
onComplete(null)
return
}
} else {
if (download != null && download.pages.size == download.downloadedImages) {
onComplete(download)
return
}
}
// Create notification
with (notificationBuilder) {
// Check if icon needs refresh
if (!isDownloading) {
setSmallIcon(android.R.drawable.stat_sys_download)
isDownloading = true
}
if (multipleDownloadThreads) {
setContentTitle(context.getString(R.string.app_name))
setContentText(context.getString(R.string.chapter_downloading_progress)
.format(initialQueueSize - queue.size, initialQueueSize))
setProgress(initialQueueSize, initialQueueSize - queue.size, false)
} else {
download?.let {
if (it.chapter.name.length >= 33)
setContentTitle(it.chapter.name.slice(IntRange(0, 30)).plus("..."))
else
setContentTitle(it.chapter.name)
setContentText(context.getString(R.string.chapter_downloading_progress)
.format(it.downloadedImages, it.pages.size))
setProgress(it.pages.size, it.downloadedImages, false)
}
}
}
// Displays the progress bar on notification
context.notificationManager.notify(notificationId, notificationBuilder.build())
}
/**
* Called when chapter is downloaded
*
* @param download download object containing download information
*/
private fun onComplete(download: Download?) {
// Create notification.
with(notificationBuilder) {
setContentTitle(download?.chapter?.name ?: context.getString(R.string.app_name))
setContentText(context.getString(R.string.update_check_notification_download_complete))
setSmallIcon(android.R.drawable.stat_sys_download_done)
setProgress(0, 0, false)
}
// Show notification.
context.notificationManager.notify(notificationId, notificationBuilder.build())
// Reset initial values
isDownloading = false
initialQueueSize = 0
}
/**
* Clears the notification message
*/
internal fun onClear() {
context.notificationManager.cancel(notificationId)
}
/**
* Called on error while downloading chapter
*
* @param error string containing error information
* @param chapter string containing chapter title
*/
internal fun onError(error: String? = null, chapter: String? = null) {
// Create notification
with(notificationBuilder) {
setContentTitle(chapter ?: context.getString(R.string.download_notifier_title_error))
setContentText(error ?: context.getString(R.string.download_notifier_unkown_error))
setSmallIcon(android.R.drawable.stat_sys_warning)
setProgress(0, 0, false)
}
context.notificationManager.notify(Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID, notificationBuilder.build())
isDownloading = false
}
}

View File

@ -82,12 +82,12 @@ class DownloadService : Service() {
stopSelf()
}
} else if (isRunning) {
downloadManager.stopDownloads()
downloadManager.stopDownloads(getString(R.string.download_notifier_text_only_wifi))
}
}
else -> {
if (isRunning) {
downloadManager.stopDownloads()
downloadManager.stopDownloads(getString(R.string.download_notifier_text_only_wifi))
}
}
}

View File

@ -5,12 +5,12 @@ import java.util.List;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.data.source.model.Page;
import eu.kanade.tachiyomi.data.source.online.OnlineSource;
import rx.subjects.PublishSubject;
public class Download {
public Source source;
public OnlineSource source;
public Manga manga;
public Chapter chapter;
public List<Page> pages;
@ -29,7 +29,7 @@ public class Download {
public static final int ERROR = 4;
public Download(Source source, Manga manga, Chapter chapter) {
public Download(OnlineSource source, Manga manga, Chapter chapter) {
this.source = source;
this.manga = manga;
this.chapter = chapter;

View File

@ -4,9 +4,9 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.source.model.Page
import rx.Observable
import rx.subjects.PublishSubject
import java.util.*
import java.util.concurrent.CopyOnWriteArrayList
class DownloadQueue : ArrayList<Download>() {
class DownloadQueue : CopyOnWriteArrayList<Download>() {
private val statusSubject = PublishSubject.create<Download>()
@ -22,12 +22,7 @@ class DownloadQueue : ArrayList<Download>() {
}
fun del(chapter: Chapter) {
for (download in this) {
if (download.chapter.id == chapter.id) {
del(download)
break
}
}
find { it.chapter.id == chapter.id }?.let { del(it) }
}
fun getActiveDownloads() =

View File

@ -0,0 +1,34 @@
package eu.kanade.tachiyomi.data.glide
import android.content.Context
import com.bumptech.glide.Glide
import com.bumptech.glide.GlideBuilder
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.module.GlideModule
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.network.NetworkHelper
import java.io.InputStream
import javax.inject.Inject
/**
* Class used to update Glide module settings
*/
class AppGlideModule : GlideModule {
@Inject lateinit var networkHelper: NetworkHelper
override fun applyOptions(context: Context, builder: GlideBuilder) {
// Set the cache size of Glide to 15 MiB
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 15 * 1024 * 1024))
}
override fun registerComponents(context: Context, glide: Glide) {
App.get(context).component.inject(this)
glide.register(GlideUrl::class.java, InputStream::class.java,
OkHttpUrlLoader.Factory(networkHelper.client))
glide.register(Manga::class.java, InputStream::class.java, MangaModelLoader.Factory())
}
}

View File

@ -0,0 +1,67 @@
package eu.kanade.tachiyomi.data.glide
import com.bumptech.glide.Priority
import com.bumptech.glide.load.data.DataFetcher
import eu.kanade.tachiyomi.data.database.models.Manga
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
/**
* A [DataFetcher] for loading a cover of a manga depending on its favorite status.
* If the manga is favorite, it tries to load the cover from our cache, and if it's not found, it
* fallbacks to network and copies it to the cache.
* If the manga is not favorite, it tries to delete the cover from our cache and always fallback
* to network for fetching.
*
* @param networkFetcher the network fetcher for this cover.
* @param file the file where this cover should be. It may exists or not.
* @param manga the manga of the cover to load.
*/
class MangaDataFetcher(private val networkFetcher: DataFetcher<InputStream>,
private val file: File,
private val manga: Manga)
: DataFetcher<InputStream> {
@Throws(Exception::class)
override fun loadData(priority: Priority): InputStream? {
if (manga.favorite) {
if (!file.exists()) {
file.parentFile.mkdirs()
networkFetcher.loadData(priority)?.let {
it.use { input ->
file.outputStream().use { output ->
input.copyTo(output)
}
}
}
}
return FileInputStream(file)
} else {
if (file.exists()) {
file.delete()
}
return networkFetcher.loadData(priority)
}
}
/**
* Returns the id for this manga's cover.
*
* Appending the file's modified date to the url, we can force Glide to skip its memory and disk
* lookup step and fetch from our custom cache. This allows us to invalidate Glide's cache when
* the file has changed. If the file doesn't exist it will append a 0.
*/
override fun getId(): String {
return manga.thumbnail_url + file.lastModified()
}
override fun cancel() {
networkFetcher.cancel()
}
override fun cleanup() {
networkFetcher.cleanup()
}
}

View File

@ -0,0 +1,119 @@
package eu.kanade.tachiyomi.data.glide
import android.content.Context
import com.bumptech.glide.Glide
import com.bumptech.glide.load.data.DataFetcher
import com.bumptech.glide.load.model.*
import com.bumptech.glide.load.model.stream.StreamModelLoader
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.source.SourceManager
import eu.kanade.tachiyomi.data.source.online.OnlineSource
import java.io.File
import java.io.InputStream
import javax.inject.Inject
/**
* A class for loading a cover associated with a [Manga] that can be present in our own cache.
* Coupled with [MangaDataFetcher], this class allows to implement the following flow:
*
* - Check in RAM LRU.
* - Check in disk LRU.
* - Check in this module.
* - Fetch from the network connection.
*
* @param context the application context.
*/
class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
/**
* Cover cache where persistent covers are stored.
*/
@Inject lateinit var coverCache: CoverCache
/**
* Source manager.
*/
@Inject lateinit var sourceManager: SourceManager
/**
* Base network loader.
*/
private val baseLoader = Glide.buildModelLoader(GlideUrl::class.java,
InputStream::class.java, context)
/**
* LRU cache whose key is the thumbnail url of the manga, and the value contains the request url
* and the file where it should be stored in case the manga is a favorite.
*/
private val modelCache = ModelCache<String, Pair<GlideUrl, File>>(100)
/**
* Map where request headers are stored for a source.
*/
private val cachedHeaders = hashMapOf<Int, LazyHeaders>()
init {
App.get(context).component.inject(this)
}
/**
* Factory class for creating [MangaModelLoader] instances.
*/
class Factory : ModelLoaderFactory<Manga, InputStream> {
override fun build(context: Context, factories: GenericLoaderFactory)
= MangaModelLoader(context)
override fun teardown() {}
}
/**
* Returns a [MangaDataFetcher] for the given manga or null if the url is empty.
*
* @param manga the model.
* @param width the width of the view where the resource will be loaded.
* @param height the height of the view where the resource will be loaded.
*/
override fun getResourceFetcher(manga: Manga,
width: Int,
height: Int): DataFetcher<InputStream>? {
// Check thumbnail is not null or empty
val url = manga.thumbnail_url
if (url.isNullOrEmpty()) {
return null
}
// Obtain the request url and the file for this url from the LRU cache, or calculate it
// and add them to the cache.
val (glideUrl, file) = modelCache.get(url, width, height) ?:
Pair(GlideUrl(url, getHeaders(manga)), coverCache.getCoverFile(url)).apply {
modelCache.put(url, width, height, this)
}
// Get the network fetcher for this request url.
val networkFetcher = baseLoader.getResourceFetcher(glideUrl, width, height)
// Return an instance of our fetcher providing the needed elements.
return MangaDataFetcher(networkFetcher, file, manga)
}
/**
* Returns the request headers for a source copying its OkHttp headers and caching them.
*
* @param manga the model.
*/
fun getHeaders(manga: Manga): Headers {
val source = sourceManager.get(manga.source) as? OnlineSource ?: return LazyHeaders.DEFAULT
return cachedHeaders.getOrPut(manga.source) {
LazyHeaders.Builder().apply {
setHeader("User-Agent", null as? String)
for ((key, value) in source.headers.toMultimap()) {
addHeader(key, value[0])
}
}.build()
}
}
}

View File

@ -8,50 +8,26 @@ import android.content.Intent
import android.os.IBinder
import android.os.PowerManager
import android.support.v4.app.NotificationCompat
import android.util.Pair
import com.github.pwittchen.reactivenetwork.library.ConnectivityStatus
import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.Constants
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.source.SourceManager
import eu.kanade.tachiyomi.data.source.online.OnlineSource
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.AndroidComponentUtil
import eu.kanade.tachiyomi.util.DeviceUtil
import eu.kanade.tachiyomi.util.notification
import eu.kanade.tachiyomi.util.notificationManager
import eu.kanade.tachiyomi.util.*
import rx.Observable
import rx.Subscription
import rx.schedulers.Schedulers
import timber.log.Timber
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
import javax.inject.Inject
// Intent key for forced library update
val UPDATE_IS_FORCED = "is_forced"
/**
* Get the start intent for [LibraryUpdateService].
* @param context the application context.
* @param isForced true when forcing library update
* @return the intent of the service.
*/
fun getIntent(context: Context, isForced: Boolean = false): Intent {
return Intent(context, LibraryUpdateService::class.java).apply {
putExtra(UPDATE_IS_FORCED, isForced)
}
}
/**
* Returns the status of the service.
* @param context the application context.
* @return true if the service is running, false otherwise.
*/
fun isRunning(context: Context): Boolean {
return AndroidComponentUtil.isServiceRunning(context, LibraryUpdateService::class.java)
}
/**
* This class will take care of updating the chapters of the manga from the library. It can be
* started calling the [start] method. If it's already running, it won't do anything.
@ -62,35 +38,84 @@ fun isRunning(context: Context): Boolean {
*/
class LibraryUpdateService : Service() {
// Dependencies injected through dagger.
/**
* Database helper.
*/
@Inject lateinit var db: DatabaseHelper
/**
* Source manager.
*/
@Inject lateinit var sourceManager: SourceManager
/**
* Preferences.
*/
@Inject lateinit var preferences: PreferencesHelper
// Wake lock that will be held until the service is destroyed.
/**
* Wake lock that will be held until the service is destroyed.
*/
private lateinit var wakeLock: PowerManager.WakeLock
// Subscription where the update is done.
/**
* Subscription where the update is done.
*/
private var subscription: Subscription? = null
/**
* Id of the library update notification.
*/
private val notificationId: Int
get() = Constants.NOTIFICATION_LIBRARY_ID
companion object {
val UPDATE_NOTIFICATION_ID = 1
/**
* Key for manual library update.
*/
const val UPDATE_IS_MANUAL = "is_manual"
/**
* Static method to start the service. It will be started only if there isn't another
* instance already running.
* @param context the application context.
* Key for category to update.
*/
@JvmStatic
fun start(context: Context, isForced: Boolean = false) {
const val UPDATE_CATEGORY = "category"
/**
* Returns the status of the service.
*
* @param context the application context.
* @return true if the service is running, false otherwise.
*/
fun isRunning(context: Context): Boolean {
return AndroidComponentUtil.isServiceRunning(context, LibraryUpdateService::class.java)
}
/**
* Starts the service. It will be started only if there isn't another instance already
* running.
*
* @param context the application context.
* @param isManual whether the update has been manually triggered.
* @param category a specific category to update, or null for all in the library.
*/
fun start(context: Context, isManual: Boolean = false, category: Category? = null) {
if (!isRunning(context)) {
context.startService(getIntent(context, isForced))
val intent = Intent(context, LibraryUpdateService::class.java).apply {
putExtra(UPDATE_IS_MANUAL, isManual)
category?.let { putExtra(UPDATE_CATEGORY, it.id) }
}
context.startService(intent)
}
}
/**
* Stops the service.
*
* @param context the application context.
*/
fun stop(context: Context) {
context.stopService(getIntent(context))
context.stopService(Intent(context, LibraryUpdateService::class.java))
}
}
@ -106,7 +131,7 @@ class LibraryUpdateService : Service() {
}
/**
* Method called when the service is destroyed. It destroy the running subscription, resets
* Method called when the service is destroyed. It destroys the running subscription, resets
* the alarm and release the wake lock.
*/
override fun onDestroy() {
@ -123,91 +148,115 @@ class LibraryUpdateService : Service() {
return null
}
/**
* Method called when the service receives an intent. In this case, the content of the intent
* is irrelevant, because everything required is fetched in [updateLibrary].
* @param intent the intent from [start].
* Method called when the service receives an intent.
*
* @param intent the start intent from.
* @param flags the flags of the command.
* @param startId the start id of this command.
* @return the start value of the command.
*/
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// If there's no network available, set a component to start this service again when
// a connection is available.
if (!DeviceUtil.isNetworkConnected(this)) {
Timber.i("Sync canceled, connection not available")
showWarningNotification(getString(R.string.notification_no_connection_title),
getString(R.string.notification_no_connection_body))
// Get connectivity status
val connection = ReactiveNetwork().getConnectivityStatus(this, true)
// Get library update restrictions
val restrictions = preferences.libraryUpdateRestriction()
// Check if users updates library manual
val isManualUpdate = intent?.getBooleanExtra(UPDATE_IS_MANUAL, false) ?: false
// Whether to cancel the update.
var cancelUpdate = false
// Check if device has internet connection
// Check if device has wifi connection if only wifi is enabled
if (connection == ConnectivityStatus.OFFLINE || (!isManualUpdate && "wifi" in restrictions
&& connection != ConnectivityStatus.WIFI_CONNECTED_HAS_INTERNET)) {
if (isManualUpdate) {
toast(R.string.notification_no_connection_title)
}
// Enable library update when connection available
AndroidComponentUtil.toggleComponent(this, SyncOnConnectionAvailable::class.java, true)
cancelUpdate = true
}
if (!isManualUpdate && "ac" in restrictions && !DeviceUtil.isPowerConnected(this)) {
AndroidComponentUtil.toggleComponent(this, SyncOnPowerConnected::class.java, true)
cancelUpdate = true
}
if (cancelUpdate) {
stopSelf(startId)
return Service.START_NOT_STICKY
}
// If user doesn't want to update while phone is not charging, cancel sync
else if (preferences.updateOnlyWhenCharging() && !(intent?.getBooleanExtra(UPDATE_IS_FORCED, false) ?: false) && !DeviceUtil.isPowerConnected(this)) {
Timber.i("Sync canceled, not connected to ac power")
// Create force library update intent
val forceIntent = getLibraryUpdateReceiverIntent(LibraryUpdateReceiver.FORCE_LIBRARY_UPDATE)
// Show warning
showWarningNotification(getString(R.string.notification_not_connected_to_ac_title),
getString(R.string.notification_not_connected_to_ac_body), forceIntent)
stopSelf(startId)
return Service.START_NOT_STICKY
}
// Stop enabled components.
AndroidComponentUtil.toggleComponent(this, SyncOnConnectionAvailable::class.java, false)
AndroidComponentUtil.toggleComponent(this, SyncOnPowerConnected::class.java, false)
// Unsubscribe from any previous subscription if needed.
subscription?.unsubscribe()
// Update favorite manga. Destroy service when completed or in case of an error.
subscription = Observable.defer { updateLibrary() }
subscription = Observable.defer { updateMangaList(getMangaToUpdate(intent)) }
.subscribeOn(Schedulers.io())
.subscribe({},
{
showNotification(getString(R.string.notification_update_error), "")
stopSelf(startId)
}, {
stopSelf(startId)
stopSelf(startId)
})
return Service.START_STICKY
}
/**
* Creates a PendingIntent for LibraryUpdate broadcast class
* @param action id of action
* Returns the list of manga to be updated.
*
* @param intent the update intent.
* @return a list of manga to update
*/
fun getLibraryUpdateReceiverIntent(action: String): PendingIntent {
return PendingIntent.getBroadcast(this, 0,
Intent(this, LibraryUpdateReceiver::class.java).apply { this.action = action }, 0)
fun getMangaToUpdate(intent: Intent?): List<Manga> {
val categoryId = intent?.getIntExtra(UPDATE_CATEGORY, -1) ?: -1
var toUpdate = if (categoryId != -1)
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
else
db.getFavoriteMangas().executeAsBlocking()
if (preferences.updateOnlyNonCompleted()) {
toUpdate = toUpdate.filter { it.status != Manga.COMPLETED }
}
return toUpdate
}
/**
* Method that updates the library. It's called in a background thread, so it's safe to do
* heavy operations or network calls here.
* Method that updates the given list of manga. It's called in a background thread, so it's safe
* to do heavy operations or network calls here.
* For each manga it calls [updateManga] and updates the notification showing the current
* progress.
*
* @param mangaToUpdate the list to update
* @return an observable delivering the progress of each update.
*/
fun updateLibrary(): Observable<Manga> {
fun updateMangaList(mangaToUpdate: List<Manga>): Observable<Manga> {
// Initialize the variables holding the progress of the updates.
val count = AtomicInteger(0)
val newUpdates = ArrayList<Manga>()
val failedUpdates = ArrayList<Manga>()
val cancelIntent = getLibraryUpdateReceiverIntent(LibraryUpdateReceiver.CANCEL_LIBRARY_UPDATE)
// Get the manga list that is going to be updated.
val allLibraryMangas = db.getFavoriteMangas().executeAsBlocking()
val toUpdate = if (!preferences.updateOnlyNonCompleted())
allLibraryMangas
else
allLibraryMangas.filter { it.status != Manga.COMPLETED }
val cancelIntent = PendingIntent.getBroadcast(this, 0,
Intent(this, CancelUpdateReceiver::class.java), 0)
// Emit each manga and update it sequentially.
return Observable.from(toUpdate)
return Observable.from(mangaToUpdate)
// Notify manga that will update.
.doOnNext { showProgressNotification(it, count.andIncrement, toUpdate.size, cancelIntent) }
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size, cancelIntent) }
// Update the chapters of the manga.
.concatMap { manga ->
updateManga(manga)
@ -235,18 +284,19 @@ class LibraryUpdateService : Service() {
/**
* Updates the chapters for the given manga and adds them to the database.
*
* @param manga the manga to update.
* @return a pair of the inserted and removed chapters.
*/
fun updateManga(manga: Manga): Observable<Pair<Int, Int>> {
val source = sourceManager.get(manga.source)
return source!!
.pullChaptersFromNetwork(manga.url)
.flatMap { db.insertOrRemoveChapters(manga, it, source) }
val source = sourceManager.get(manga.source) as? OnlineSource ?: return Observable.empty()
return source.fetchChapterList(manga)
.map { syncChaptersWithSource(db, it, manga, source) }
}
/**
* Returns the text that will be displayed in the notification when there are new chapters.
*
* @param updates a list of manga that contains new chapters.
* @param failedUpdates a list of manga that failed to update.
* @return the body of the notification to display.
@ -295,57 +345,39 @@ class LibraryUpdateService : Service() {
/**
* Shows the notification with the given title and body.
*
* @param title the title of the notification.
* @param body the body of the notification.
*/
private fun showNotification(title: String, body: String) {
val n = notification() {
notificationManager.notify(notificationId, notification() {
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
setContentTitle(title)
setContentText(body)
}
notificationManager.notify(UPDATE_NOTIFICATION_ID, n)
})
}
/**
* Shows the notification containing the currently updating manga and the progress.
*
* @param manga the manga that's being updated.
* @param current the current progress.
* @param total the total progress.
*/
private fun showProgressNotification(manga: Manga, current: Int, total: Int, cancelIntent: PendingIntent) {
val n = notification() {
notificationManager.notify(notificationId, notification() {
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
setContentTitle(manga.title)
setProgress(total, current, false)
setOngoing(true)
addAction(R.drawable.ic_clear_grey_24dp_img, getString(android.R.string.cancel), cancelIntent)
}
notificationManager.notify(UPDATE_NOTIFICATION_ID, n)
})
}
/**
* Show warning message when library can't be updated
* @param warningTitle title of warning
* @param warningBody warning information
* @param pendingIntent Intent called when action clicked
*/
private fun showWarningNotification(warningTitle: String, warningBody: String, pendingIntent: PendingIntent? = null) {
val n = notification() {
setSmallIcon(R.drawable.ic_warning_white_24dp_img)
setContentTitle(warningTitle)
setStyle(NotificationCompat.BigTextStyle().bigText(warningBody))
setContentIntent(notificationIntent)
if (pendingIntent != null) {
addAction(R.drawable.ic_refresh_grey_24dp_img, getString(R.string.action_force), pendingIntent)
}
setAutoCancel(true)
}
notificationManager.notify(UPDATE_NOTIFICATION_ID, n)
}
/**
* Shows the notification containing the result of the update done by the service.
*
* @param updates a list of manga with new updates.
* @param failed a list of manga that failed to update.
*/
@ -353,21 +385,20 @@ class LibraryUpdateService : Service() {
val title = getString(R.string.notification_update_completed)
val body = getUpdatedMangasBody(updates, failed)
val n = notification() {
notificationManager.notify(notificationId, notification() {
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
setContentTitle(title)
setStyle(NotificationCompat.BigTextStyle().bigText(body))
setContentIntent(notificationIntent)
setAutoCancel(true)
}
notificationManager.notify(UPDATE_NOTIFICATION_ID, n)
})
}
/**
* Cancels the notification.
*/
private fun cancelNotification() {
notificationManager.cancel(UPDATE_NOTIFICATION_ID)
notificationManager.cancel(notificationId)
}
/**
@ -385,45 +416,48 @@ class LibraryUpdateService : Service() {
* network changes.
*/
class SyncOnConnectionAvailable : BroadcastReceiver() {
/**
* Method called when a network change occurs.
*
* @param context the application context.
* @param intent the intent received.
*/
override fun onReceive(context: Context, intent: Intent) {
if (DeviceUtil.isNetworkConnected(context)) {
AndroidComponentUtil.toggleComponent(context, this.javaClass, false)
context.startService(getIntent(context))
start(context)
}
}
}
/**
* Class that triggers the library to update.
* Class that triggers the library to update when connected to power.
*/
class LibraryUpdateReceiver : BroadcastReceiver() {
companion object {
// Cancel library update action
val CANCEL_LIBRARY_UPDATE = "eu.kanade.CANCEL_LIBRARY_UPDATE"
// Force library update
val FORCE_LIBRARY_UPDATE = "eu.kanade.FORCE_LIBRARY_UPDATE"
class SyncOnPowerConnected: BroadcastReceiver() {
/**
* Method called when AC is connected.
*
* @param context the application context.
* @param intent the intent received.
*/
override fun onReceive(context: Context, intent: Intent) {
AndroidComponentUtil.toggleComponent(context, this.javaClass, false)
start(context)
}
}
/**
* Class that stops updating the library.
*/
class CancelUpdateReceiver : BroadcastReceiver() {
/**
* Method called when user wants a library update.
* @param context the application context.
* @param intent the intent received.
*/
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
CANCEL_LIBRARY_UPDATE -> {
LibraryUpdateService.stop(context)
context.notificationManager.cancel(UPDATE_NOTIFICATION_ID)
}
FORCE_LIBRARY_UPDATE -> LibraryUpdateService.start(context, true)
}
LibraryUpdateService.stop(context)
context.notificationManager.cancel(Constants.NOTIFICATION_LIBRARY_ID)
}
}
}

View File

@ -1,23 +1,18 @@
package eu.kanade.tachiyomi.data.mangasync
import android.content.Context
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService
import eu.kanade.tachiyomi.data.mangasync.services.MyAnimeList
import eu.kanade.tachiyomi.data.mangasync.myanimelist.MyAnimeList
class MangaSyncManager(private val context: Context) {
val services: List<MangaSyncService>
val myAnimeList: MyAnimeList
companion object {
const val MYANIMELIST = 1
}
init {
myAnimeList = MyAnimeList(context, MYANIMELIST)
services = listOf(myAnimeList)
}
val myAnimeList = MyAnimeList(context, MYANIMELIST)
fun getService(id: Int): MangaSyncService = services.find { it.id == id }!!
val services = listOf(myAnimeList)
fun getService(id: Int) = services.find { it.id == id }
}

View File

@ -0,0 +1,56 @@
package eu.kanade.tachiyomi.data.mangasync
import android.content.Context
import android.support.annotation.CallSuper
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.data.database.models.MangaSync
import eu.kanade.tachiyomi.data.network.NetworkHelper
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import okhttp3.OkHttpClient
import rx.Completable
import rx.Observable
import javax.inject.Inject
abstract class MangaSyncService(private val context: Context, val id: Int) {
@Inject lateinit var preferences: PreferencesHelper
@Inject lateinit var networkService: NetworkHelper
init {
App.get(context).component.inject(this)
}
open val client: OkHttpClient
get() = networkService.client
// Name of the manga sync service to display
abstract val name: String
abstract fun login(username: String, password: String): Completable
open val isLogged: Boolean
get() = !getUsername().isEmpty() &&
!getPassword().isEmpty()
abstract fun add(manga: MangaSync): Observable<MangaSync>
abstract fun update(manga: MangaSync): Observable<MangaSync>
abstract fun bind(manga: MangaSync): Observable<MangaSync>
abstract fun getStatus(status: Int): String
fun saveCredentials(username: String, password: String) {
preferences.setMangaSyncCredentials(this, username, password)
}
@CallSuper
open fun logout() {
preferences.setMangaSyncCredentials(this, "", "")
}
fun getUsername() = preferences.mangaSyncUsername(this)
fun getPassword() = preferences.mangaSyncPassword(this)
}

View File

@ -48,15 +48,13 @@ class UpdateMangaSyncService : Service() {
private fun updateLastChapterRead(mangaSync: MangaSync, startId: Int) {
val sync = syncManager.getService(mangaSync.sync_id)
if (sync == null) {
stopSelf(startId)
return
}
subscriptions.add(Observable.defer { sync.update(mangaSync) }
.flatMap {
if (it.isSuccessful) {
db.insertMangaSync(mangaSync).asRxObservable()
} else {
Observable.error(Exception("Could not update manga in remote service"))
}
}
.flatMap { db.insertMangaSync(mangaSync).asRxObservable() }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ stopSelf(startId) },

View File

@ -1,38 +0,0 @@
package eu.kanade.tachiyomi.data.mangasync.base
import android.content.Context
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.data.database.models.MangaSync
import eu.kanade.tachiyomi.data.network.NetworkHelper
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import okhttp3.Response
import rx.Observable
import javax.inject.Inject
abstract class MangaSyncService(private val context: Context, val id: Int) {
@Inject lateinit var preferences: PreferencesHelper
@Inject lateinit var networkService: NetworkHelper
init {
App.get(context).component.inject(this)
}
// Name of the manga sync service to display
abstract val name: String
abstract fun login(username: String, password: String): Observable<Boolean>
open val isLogged: Boolean
get() = !preferences.getMangaSyncUsername(this).isEmpty() &&
!preferences.getMangaSyncPassword(this).isEmpty()
abstract fun update(manga: MangaSync): Observable<Response>
abstract fun add(manga: MangaSync): Observable<Response>
abstract fun bind(manga: MangaSync): Observable<Response>
abstract fun getStatus(status: Int): String
}

View File

@ -0,0 +1,222 @@
package eu.kanade.tachiyomi.data.mangasync.myanimelist
import android.content.Context
import android.net.Uri
import android.util.Xml
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.MangaSync
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
import eu.kanade.tachiyomi.data.network.GET
import eu.kanade.tachiyomi.data.network.POST
import eu.kanade.tachiyomi.data.network.asObservable
import eu.kanade.tachiyomi.util.selectInt
import eu.kanade.tachiyomi.util.selectText
import okhttp3.Credentials
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.RequestBody
import org.jsoup.Jsoup
import org.xmlpull.v1.XmlSerializer
import rx.Completable
import rx.Observable
import java.io.StringWriter
class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(context, id) {
private lateinit var headers: Headers
companion object {
val BASE_URL = "http://myanimelist.net"
private val ENTRY_TAG = "entry"
private val CHAPTER_TAG = "chapter"
private val SCORE_TAG = "score"
private val STATUS_TAG = "status"
val READING = 1
val COMPLETED = 2
val ON_HOLD = 3
val DROPPED = 4
val PLAN_TO_READ = 6
val DEFAULT_STATUS = READING
val DEFAULT_SCORE = 0
}
init {
val username = getUsername()
val password = getPassword()
if (!username.isEmpty() && !password.isEmpty()) {
createHeaders(username, password)
}
}
override val name: String
get() = "MyAnimeList"
fun getLoginUrl() = Uri.parse(BASE_URL).buildUpon()
.appendEncodedPath("api/account/verify_credentials.xml")
.toString()
fun getSearchUrl(query: String) = Uri.parse(BASE_URL).buildUpon()
.appendEncodedPath("api/manga/search.xml")
.appendQueryParameter("q", query)
.toString()
fun getListUrl(username: String) = Uri.parse(BASE_URL).buildUpon()
.appendPath("malappinfo.php")
.appendQueryParameter("u", username)
.appendQueryParameter("status", "all")
.appendQueryParameter("type", "manga")
.toString()
fun getUpdateUrl(manga: MangaSync) = Uri.parse(BASE_URL).buildUpon()
.appendEncodedPath("api/mangalist/update")
.appendPath("${manga.remote_id}.xml")
.toString()
fun getAddUrl(manga: MangaSync) = Uri.parse(BASE_URL).buildUpon()
.appendEncodedPath("api/mangalist/add")
.appendPath("${manga.remote_id}.xml")
.toString()
override fun login(username: String, password: String): Completable {
createHeaders(username, password)
return client.newCall(GET(getLoginUrl(), headers))
.asObservable()
.doOnNext { it.close() }
.doOnNext { if (it.code() != 200) throw Exception("Login error") }
.toCompletable()
}
fun search(query: String): Observable<List<MangaSync>> {
return client.newCall(GET(getSearchUrl(query), headers))
.asObservable()
.map { Jsoup.parse(it.body().string()) }
.flatMap { Observable.from(it.select("entry")) }
.filter { it.select("type").text() != "Novel" }
.map {
MangaSync.create(this).apply {
title = it.selectText("title")
remote_id = it.selectInt("id")
total_chapters = it.selectInt("chapters")
}
}
.toList()
}
// MAL doesn't support score with decimals
fun getList(): Observable<List<MangaSync>> {
return networkService.forceCacheClient
.newCall(GET(getListUrl(getUsername()), headers))
.asObservable()
.map { Jsoup.parse(it.body().string()) }
.flatMap { Observable.from(it.select("manga")) }
.map {
MangaSync.create(this).apply {
title = it.selectText("series_title")
remote_id = it.selectInt("series_mangadb_id")
last_chapter_read = it.selectInt("my_read_chapters")
status = it.selectInt("my_status")
score = it.selectInt("my_score").toFloat()
total_chapters = it.selectInt("series_chapters")
}
}
.toList()
}
override fun update(manga: MangaSync): Observable<MangaSync> {
return Observable.defer {
if (manga.total_chapters != 0 && manga.last_chapter_read == manga.total_chapters) {
manga.status = COMPLETED
}
client.newCall(POST(getUpdateUrl(manga), headers, getMangaPostPayload(manga)))
.asObservable()
.doOnNext { it.close() }
.doOnNext { if (!it.isSuccessful) throw Exception("Could not update manga") }
.map { manga }
}
}
override fun add(manga: MangaSync): Observable<MangaSync> {
return Observable.defer {
client.newCall(POST(getAddUrl(manga), headers, getMangaPostPayload(manga)))
.asObservable()
.doOnNext { it.close() }
.doOnNext { if (!it.isSuccessful) throw Exception("Could not add manga") }
.map { manga }
}
}
private fun getMangaPostPayload(manga: MangaSync): RequestBody {
val xml = Xml.newSerializer()
val writer = StringWriter()
with(xml) {
setOutput(writer)
startDocument("UTF-8", false)
startTag("", ENTRY_TAG)
// Last chapter read
if (manga.last_chapter_read != 0) {
inTag(CHAPTER_TAG, manga.last_chapter_read.toString())
}
// Manga status in the list
inTag(STATUS_TAG, manga.status.toString())
// Manga score
inTag(SCORE_TAG, manga.score.toString())
endTag("", ENTRY_TAG)
endDocument()
}
val form = FormBody.Builder()
form.add("data", writer.toString())
return form.build()
}
fun XmlSerializer.inTag(tag: String, body: String, namespace: String = "") {
startTag(namespace, tag)
text(body)
endTag(namespace, tag)
}
override fun bind(manga: MangaSync): Observable<MangaSync> {
return getList()
.flatMap { userlist ->
manga.sync_id = id
val mangaFromList = userlist.find { it.remote_id == manga.remote_id }
if (mangaFromList != null) {
manga.copyPersonalFrom(mangaFromList)
update(manga)
} else {
// Set default fields if it's not found in the list
manga.score = DEFAULT_SCORE.toFloat()
manga.status = DEFAULT_STATUS
add(manga)
}
}
}
override fun getStatus(status: Int): String = with(context) {
when (status) {
READING -> getString(R.string.reading)
COMPLETED -> getString(R.string.completed)
ON_HOLD -> getString(R.string.on_hold)
DROPPED -> getString(R.string.dropped)
PLAN_TO_READ -> getString(R.string.plan_to_read)
else -> ""
}
}
fun createHeaders(username: String, password: String) {
val builder = Headers.Builder()
builder.add("Authorization", Credentials.basic(username, password))
builder.add("User-Agent", "api-indiv-9F93C52A963974CF674325391990191C")
headers = builder.build()
}
}

View File

@ -1,216 +0,0 @@
package eu.kanade.tachiyomi.data.mangasync.services
import android.content.Context
import android.net.Uri
import android.util.Xml
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.MangaSync
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService
import eu.kanade.tachiyomi.data.network.get
import eu.kanade.tachiyomi.data.network.post
import eu.kanade.tachiyomi.util.selectInt
import eu.kanade.tachiyomi.util.selectText
import okhttp3.*
import org.jsoup.Jsoup
import org.xmlpull.v1.XmlSerializer
import rx.Observable
import java.io.StringWriter
fun XmlSerializer.inTag(tag: String, body: String, namespace: String = "") {
startTag(namespace, tag)
text(body)
endTag(namespace, tag)
}
class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(context, id) {
private lateinit var headers: Headers
private lateinit var username: String
companion object {
val BASE_URL = "http://myanimelist.net"
private val ENTRY_TAG = "entry"
private val CHAPTER_TAG = "chapter"
private val SCORE_TAG = "score"
private val STATUS_TAG = "status"
val READING = 1
val COMPLETED = 2
val ON_HOLD = 3
val DROPPED = 4
val PLAN_TO_READ = 6
val DEFAULT_STATUS = READING
val DEFAULT_SCORE = 0
}
init {
val username = preferences.getMangaSyncUsername(this)
val password = preferences.getMangaSyncPassword(this)
if (!username.isEmpty() && !password.isEmpty()) {
createHeaders(username, password)
}
}
override val name: String
get() = "MyAnimeList"
fun getLoginUrl(): String {
return Uri.parse(BASE_URL).buildUpon()
.appendEncodedPath("api/account/verify_credentials.xml")
.toString()
}
override fun login(username: String, password: String): Observable<Boolean> {
createHeaders(username, password)
return networkService.request(get(getLoginUrl(), headers))
.map { it.code() == 200 }
}
fun getSearchUrl(query: String): String {
return Uri.parse(BASE_URL).buildUpon()
.appendEncodedPath("api/manga/search.xml")
.appendQueryParameter("q", query)
.toString()
}
fun search(query: String): Observable<List<MangaSync>> {
return networkService.requestBody(get(getSearchUrl(query), headers))
.map { Jsoup.parse(it) }
.flatMap { Observable.from(it.select("entry")) }
.filter { it.select("type").text() != "Novel" }
.map {
val manga = MangaSync.create(this)
manga.title = it.selectText("title")
manga.remote_id = it.selectInt("id")
manga.total_chapters = it.selectInt("chapters")
manga
}
.toList()
}
fun getListUrl(username: String): String {
return Uri.parse(BASE_URL).buildUpon()
.appendPath("malappinfo.php")
.appendQueryParameter("u", username)
.appendQueryParameter("status", "all")
.appendQueryParameter("type", "manga")
.toString()
}
// MAL doesn't support score with decimals
fun getList(): Observable<List<MangaSync>> {
return networkService.requestBody(get(getListUrl(username), headers), true)
.map { Jsoup.parse(it) }
.flatMap { Observable.from(it.select("manga")) }
.map {
val manga = MangaSync.create(this)
manga.title = it.selectText("series_title")
manga.remote_id = it.selectInt("series_mangadb_id")
manga.last_chapter_read = it.selectInt("my_read_chapters")
manga.status = it.selectInt("my_status")
manga.score = it.selectInt("my_score").toFloat()
manga.total_chapters = it.selectInt("series_chapters")
manga
}
.toList()
}
fun getUpdateUrl(manga: MangaSync): String {
return Uri.parse(BASE_URL).buildUpon()
.appendEncodedPath("api/mangalist/update")
.appendPath(manga.remote_id.toString() + ".xml")
.toString()
}
override fun update(manga: MangaSync): Observable<Response> {
return Observable.defer {
if (manga.total_chapters != 0 && manga.last_chapter_read == manga.total_chapters) {
manga.status = COMPLETED
}
networkService.request(post(getUpdateUrl(manga), headers, getMangaPostPayload(manga)))
}
}
fun getAddUrl(manga: MangaSync): String {
return Uri.parse(BASE_URL).buildUpon()
.appendEncodedPath("api/mangalist/add")
.appendPath(manga.remote_id.toString() + ".xml")
.toString()
}
override fun add(manga: MangaSync): Observable<Response> {
return Observable.defer {
networkService.request(post(getAddUrl(manga), headers, getMangaPostPayload(manga)))
}
}
private fun getMangaPostPayload(manga: MangaSync): RequestBody {
val xml = Xml.newSerializer()
val writer = StringWriter()
with(xml) {
setOutput(writer)
startDocument("UTF-8", false)
startTag("", ENTRY_TAG)
// Last chapter read
if (manga.last_chapter_read != 0) {
inTag(CHAPTER_TAG, manga.last_chapter_read.toString())
}
// Manga status in the list
inTag(STATUS_TAG, manga.status.toString())
// Manga score
inTag(SCORE_TAG, manga.score.toString())
endTag("", ENTRY_TAG)
endDocument()
}
val form = FormBody.Builder()
form.add("data", writer.toString())
return form.build()
}
override fun bind(manga: MangaSync): Observable<Response> {
return getList()
.flatMap {
manga.sync_id = id
for (remoteManga in it) {
if (remoteManga.remote_id == manga.remote_id) {
// Manga is already in the list
manga.copyPersonalFrom(remoteManga)
return@flatMap update(manga)
}
}
// Set default fields if it's not found in the list
manga.score = DEFAULT_SCORE.toFloat()
manga.status = DEFAULT_STATUS
return@flatMap add(manga)
}
}
override fun getStatus(status: Int): String = with(context) {
when (status) {
READING -> getString(R.string.reading)
COMPLETED -> getString(R.string.completed)
ON_HOLD -> getString(R.string.on_hold)
DROPPED -> getString(R.string.dropped)
PLAN_TO_READ -> getString(R.string.plan_to_read)
else -> ""
}
}
fun createHeaders(username: String, password: String) {
this.username = username
val builder = Headers.Builder()
builder.add("Authorization", Credentials.basic(username, password))
builder.add("User-Agent", "api-indiv-9F93C52A963974CF674325391990191C")
headers = builder.build()
}
}

View File

@ -0,0 +1,81 @@
package eu.kanade.tachiyomi.data.network
import com.squareup.duktape.Duktape
import okhttp3.HttpUrl
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
class CloudflareInterceptor(private val cookies: PersistentCookieStore) : Interceptor {
//language=RegExp
private val operationPattern = Regex("""setTimeout\(function\(\)\{\s+(var (?:\w,)+f.+?\r?\n[\s\S]+?a\.value =.+?)\r?\n""")
//language=RegExp
private val passPattern = Regex("""name="pass" value="(.+?)"""")
//language=RegExp
private val challengePattern = Regex("""name="jschl_vc" value="(\w+)"""")
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
// Check if we already solved a challenge
if (response.code() != 503 &&
cookies.get(response.request().url()).any { it.name() == "cf_clearance" }) {
return response
}
// Check if Cloudflare anti-bot is on
if ("URL=/cdn-cgi/" in response.header("Refresh", "")
&& response.header("Server", "") == "cloudflare-nginx") {
return chain.proceed(resolveChallenge(response))
}
return response
}
private fun resolveChallenge(response: Response): Request {
val duktape = Duktape.create()
try {
val originalRequest = response.request()
val domain = originalRequest.url().host()
val content = response.body().string()
// CloudFlare requires waiting 4 seconds before resolving the challenge
Thread.sleep(4000)
val operation = operationPattern.find(content)?.groups?.get(1)?.value
val challenge = challengePattern.find(content)?.groups?.get(1)?.value
val pass = passPattern.find(content)?.groups?.get(1)?.value
if (operation == null || challenge == null || pass == null) {
throw RuntimeException("Failed resolving Cloudflare challenge")
}
val js = operation
//language=RegExp
.replace(Regex("""a\.value =(.+?) \+ .+?;"""), "$1")
//language=RegExp
.replace(Regex("""\s{3,}[a-z](?: = |\.).+"""), "")
.replace("\n", "")
// Duktape can only return strings, so the result has to be converted to string first
val result = duktape.evaluate("$js.toString()").toInt()
val answer = "${result + domain.length}"
val url = HttpUrl.parse("http://$domain/cdn-cgi/l/chk_jschl").newBuilder()
.addQueryParameter("jschl_vc", challenge)
.addQueryParameter("pass", pass)
.addQueryParameter("jschl_answer", answer)
.toString()
val referer = originalRequest.url().toString()
return GET(url, originalRequest.headers().newBuilder().add("Referer", referer).build())
} finally {
duktape.close()
}
}
}

View File

@ -1,78 +1,38 @@
package eu.kanade.tachiyomi.data.network
import android.content.Context
import okhttp3.*
import rx.Observable
import okhttp3.Cache
import okhttp3.OkHttpClient
import java.io.File
import java.net.CookieManager
import java.net.CookiePolicy
import java.net.CookieStore
class NetworkHelper(context: Context) {
private val client: OkHttpClient
private val forceCacheClient: OkHttpClient
private val cookieManager: CookieManager
private val forceCacheInterceptor = { chain: Interceptor.Chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.removeHeader("Pragma")
.header("Cache-Control", "max-age=" + 600)
.build()
}
private val cacheDir = File(context.cacheDir, "network_cache")
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
private val cacheDir = "network_cache"
init {
val cacheDir = File(context.cacheDir, cacheDir)
private val cookieManager = PersistentCookieJar(context)
cookieManager = CookieManager()
cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL)
val client = OkHttpClient.Builder()
.cookieJar(cookieManager)
.cache(Cache(cacheDir, cacheSize))
.build()
client = OkHttpClient.Builder()
.cookieJar(JavaNetCookieJar(cookieManager))
.cache(Cache(cacheDir, cacheSize))
.build()
val forceCacheClient = client.newBuilder()
.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.removeHeader("Pragma")
.header("Cache-Control", "max-age=600")
.build()
}
.build()
forceCacheClient = client.newBuilder()
.addNetworkInterceptor(forceCacheInterceptor)
.build()
}
val cloudflareClient = client.newBuilder()
.addInterceptor(CloudflareInterceptor(cookies))
.build()
@JvmOverloads
fun request(request: Request, forceCache: Boolean = false): Observable<Response> {
return Observable.fromCallable {
val c = if (forceCache) forceCacheClient else client
c.newCall(request).execute()
}
}
@JvmOverloads
fun requestBody(request: Request, forceCache: Boolean = false): Observable<String> {
return request(request, forceCache)
.map { it.body().string() }
}
fun requestBodyProgress(request: Request, listener: ProgressListener): Observable<Response> {
return Observable.fromCallable {
val progressClient = client.newBuilder()
.cache(null)
.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.body(ProgressResponseBody(originalResponse.body(), listener))
.build()
}
.build()
progressClient.newCall(request).execute()
}.retry(1)
}
val cookies: CookieStore
get() = cookieManager.cookieStore
val cookies: PersistentCookieStore
get() = cookieManager.store
}

View File

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.data.network
import okhttp3.Call
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import rx.subscriptions.Subscriptions
import java.io.IOException
fun Call.asObservable(): Observable<Response> {
return Observable.create { subscriber ->
subscriber.add(Subscriptions.create { cancel() })
try {
val response = execute()
if (!subscriber.isUnsubscribed) {
subscriber.onNext(response)
subscriber.onCompleted()
}
} catch (error: IOException) {
if (!subscriber.isUnsubscribed) {
subscriber.onError(error)
}
}
}
}
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
val progressClient = newBuilder()
.cache(null)
.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.body(ProgressResponseBody(originalResponse.body(), listener))
.build()
}
.build()
return progressClient.newCall(request)
}

View File

@ -0,0 +1,19 @@
package eu.kanade.tachiyomi.data.network
import android.content.Context
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl
class PersistentCookieJar(context: Context) : CookieJar {
val store = PersistentCookieStore(context)
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
store.addAll(url, cookies)
}
override fun loadForRequest(url: HttpUrl): List<Cookie> {
return store.get(url)
}
}

View File

@ -0,0 +1,75 @@
package eu.kanade.tachiyomi.data.network
import android.content.Context
import okhttp3.Cookie
import okhttp3.HttpUrl
import java.net.URI
import java.util.concurrent.ConcurrentHashMap
class PersistentCookieStore(context: Context) {
private val cookieMap = ConcurrentHashMap<String, List<Cookie>>()
private val prefs = context.getSharedPreferences("cookie_store", Context.MODE_PRIVATE)
init {
for ((key, value) in prefs.all) {
@Suppress("UNCHECKED_CAST")
val cookies = value as? Set<String>
if (cookies != null) {
try {
val url = HttpUrl.parse("http://$key")
val nonExpiredCookies = cookies.map { Cookie.parse(url, it) }
.filter { !it.hasExpired() }
cookieMap.put(key, nonExpiredCookies)
} catch (e: Exception) {
// Ignore
}
}
}
}
fun addAll(url: HttpUrl, cookies: List<Cookie>) {
synchronized(this) {
val key = url.uri().host
// Append or replace the cookies for this domain.
val cookiesForDomain = cookieMap[key].orEmpty().toMutableList()
for (cookie in cookies) {
// Find a cookie with the same name. Replace it if found, otherwise add a new one.
val pos = cookiesForDomain.indexOfFirst { it.name() == cookie.name() }
if (pos == -1) {
cookiesForDomain.add(cookie)
} else {
cookiesForDomain[pos] = cookie
}
}
cookieMap.put(key, cookiesForDomain)
// Get cookies to be stored in disk
val newValues = cookiesForDomain.asSequence()
.filter { it.persistent() && !it.hasExpired() }
.map { it.toString() }
.toSet()
prefs.edit().putStringSet(key, newValues).apply()
}
}
fun removeAll() {
synchronized(this) {
prefs.edit().clear().apply()
cookieMap.clear()
}
}
fun get(url: HttpUrl) = get(url.uri().host)
fun get(uri: URI) = get(uri.host)
private fun get(url: String): List<Cookie> {
return cookieMap[url].orEmpty().filter { !it.hasExpired() }
}
private fun Cookie.hasExpired() = System.currentTimeMillis() >= expiresAt()
}

View File

@ -1,14 +1,13 @@
package eu.kanade.tachiyomi.data.network
import okhttp3.*
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeUnit.MINUTES
private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, TimeUnit.MINUTES).build()
private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, MINUTES).build()
private val DEFAULT_HEADERS = Headers.Builder().build()
private val DEFAULT_BODY: RequestBody = FormBody.Builder().build()
@JvmOverloads
fun get(url: String,
fun GET(url: String,
headers: Headers = DEFAULT_HEADERS,
cache: CacheControl = DEFAULT_CACHE_CONTROL): Request {
@ -19,8 +18,7 @@ fun get(url: String,
.build()
}
@JvmOverloads
fun post(url: String,
fun POST(url: String,
headers: Headers = DEFAULT_HEADERS,
body: RequestBody = DEFAULT_BODY,
cache: CacheControl = DEFAULT_CACHE_CONTROL): Request {

View File

@ -0,0 +1,91 @@
package eu.kanade.tachiyomi.data.preference
import android.content.Context
import eu.kanade.tachiyomi.R
/**
* This class stores the keys for the preferences in the application. Most of them are defined
* in the file "keys.xml". By using this class we can define preferences in one place and get them
* referenced here.
*/
class PreferenceKeys(context: Context) {
val rotation = context.getString(R.string.pref_rotation_type_key)
val enableTransitions = context.getString(R.string.pref_enable_transitions_key)
val showPageNumber = context.getString(R.string.pref_show_page_number_key)
val hideStatusBar = context.getString(R.string.pref_hide_status_bar_key)
val keepScreenOn = context.getString(R.string.pref_keep_screen_on_key)
val customBrightness = context.getString(R.string.pref_custom_brightness_key)
val customBrightnessValue = context.getString(R.string.pref_custom_brightness_value_key)
val defaultViewer = context.getString(R.string.pref_default_viewer_key)
val imageScaleType = context.getString(R.string.pref_image_scale_type_key)
val imageDecoder = context.getString(R.string.pref_image_decoder_key)
val zoomStart = context.getString(R.string.pref_zoom_start_key)
val readerTheme = context.getString(R.string.pref_reader_theme_key)
val readWithTapping = context.getString(R.string.pref_read_with_tapping_key)
val readWithVolumeKeys = context.getString(R.string.pref_read_with_volume_keys_key)
val reencodeImage = context.getString(R.string.pref_reencode_key)
val portraitColumns = context.getString(R.string.pref_library_columns_portrait_key)
val landscapeColumns = context.getString(R.string.pref_library_columns_landscape_key)
val updateOnlyNonCompleted = context.getString(R.string.pref_update_only_non_completed_key)
val autoUpdateMangaSync = context.getString(R.string.pref_auto_update_manga_sync_key)
val askUpdateMangaSync = context.getString(R.string.pref_ask_update_manga_sync_key)
val lastUsedCatalogueSource = context.getString(R.string.pref_last_catalogue_source_key)
val lastUsedCategory = context.getString(R.string.pref_last_used_category_key)
val seamlessMode = context.getString(R.string.pref_seamless_mode_key)
val catalogueAsList = context.getString(R.string.pref_display_catalogue_as_list)
val enabledLanguages = context.getString(R.string.pref_source_languages)
val downloadsDirectory = context.getString(R.string.pref_download_directory_key)
val downloadThreads = context.getString(R.string.pref_download_slots_key)
val downloadOnlyOverWifi = context.getString(R.string.pref_download_only_over_wifi_key)
val removeAfterRead = context.getString(R.string.pref_remove_after_read_key)
val removeAfterReadPrevious = context.getString(R.string.pref_remove_after_read_previous_key)
val removeAfterMarkedAsRead = context.getString(R.string.pref_remove_after_marked_as_read_key)
val libraryUpdateInterval = context.getString(R.string.pref_library_update_interval_key)
val libraryUpdateRestriction = context.getString(R.string.pref_library_update_restriction_key)
val filterDownloaded = context.getString(R.string.pref_filter_downloaded_key)
val filterUnread = context.getString(R.string.pref_filter_unread_key)
fun sourceUsername(sourceId: Int) = "pref_source_username_$sourceId"
fun sourcePassword(sourceId: Int) = "pref_source_password_$sourceId"
fun syncUsername(syncId: Int) = "pref_mangasync_username_$syncId"
fun syncPassword(syncId: Int) = "pref_mangasync_password_$syncId"
}

View File

@ -6,8 +6,8 @@ import android.preference.PreferenceManager
import com.f2prateek.rx.preferences.Preference
import com.f2prateek.rx.preferences.RxSharedPreferences
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService
import eu.kanade.tachiyomi.data.source.base.Source
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
import eu.kanade.tachiyomi.data.source.Source
import java.io.File
import java.io.IOException
@ -15,36 +15,35 @@ fun <T> Preference<T>.getOrDefault(): T = get() ?: defaultValue()!!
class PreferencesHelper(private val context: Context) {
val keys = PreferenceKeys(context)
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
private val rxPrefs = RxSharedPreferences.create(prefs)
private val defaultDownloadsDir: File
private val defaultDownloadsDir = File(Environment.getExternalStorageDirectory().absolutePath +
File.separator + context.getString(R.string.app_name), "downloads")
init {
defaultDownloadsDir = File(Environment.getExternalStorageDirectory().absolutePath +
File.separator + context.getString(R.string.app_name), "downloads")
// Don't display downloaded chapters in gallery apps creating a ".nomedia" file
try {
File(downloadsDirectory, ".nomedia").createNewFile()
File(downloadsDirectory().getOrDefault(), ".nomedia").createNewFile()
} catch (e: IOException) {
/* Ignore */
}
}
companion object {
const val SOURCE_ACCOUNT_USERNAME = "pref_source_username_"
const val SOURCE_ACCOUNT_PASSWORD = "pref_source_password_"
const val MANGASYNC_ACCOUNT_USERNAME = "pref_mangasync_username_"
const val MANGASYNC_ACCOUNT_PASSWORD = "pref_mangasync_password_"
fun getLibraryUpdateInterval(context: Context): Int {
return PreferenceManager.getDefaultSharedPreferences(context).getInt(
context.getString(R.string.pref_library_update_interval_key), 0)
}
fun getAutomaticUpdateStatus(context: Context): Boolean {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(
context.getString(R.string.pref_enable_automatic_updates), false)
}
@JvmStatic
fun getTheme(context: Context): Int {
return PreferenceManager.getDefaultSharedPreferences(context).getInt(
@ -52,165 +51,102 @@ class PreferencesHelper(private val context: Context) {
}
}
private fun getKey(keyResource: Int): String {
return context.getString(keyResource)
}
fun clear() {
prefs.edit().clear().apply()
}
fun rotation(): Preference<Int> {
return rxPrefs.getInteger(getKey(R.string.pref_rotation_type_key), 1)
}
fun rotation() = rxPrefs.getInteger(keys.rotation, 1)
fun enableTransitions(): Preference<Boolean> {
return rxPrefs.getBoolean(getKey(R.string.pref_enable_transitions_key), true)
}
fun enableTransitions() = rxPrefs.getBoolean(keys.enableTransitions, true)
fun showPageNumber(): Preference<Boolean> {
return rxPrefs.getBoolean(getKey(R.string.pref_show_page_number_key), true)
}
fun showPageNumber() = rxPrefs.getBoolean(keys.showPageNumber, true)
fun hideStatusBar(): Preference<Boolean> {
return rxPrefs.getBoolean(getKey(R.string.pref_hide_status_bar_key), true)
}
fun hideStatusBar() = rxPrefs.getBoolean(keys.hideStatusBar, true)
fun keepScreenOn(): Preference<Boolean> {
return rxPrefs.getBoolean(getKey(R.string.pref_keep_screen_on_key), true)
}
fun keepScreenOn() = rxPrefs.getBoolean(keys.keepScreenOn, true)
fun customBrightness(): Preference<Boolean> {
return rxPrefs.getBoolean(getKey(R.string.pref_custom_brightness_key), false)
}
fun customBrightness() = rxPrefs.getBoolean(keys.customBrightness, false)
fun customBrightnessValue(): Preference<Float> {
return rxPrefs.getFloat(getKey(R.string.pref_custom_brightness_value_key), 0f)
}
fun customBrightnessValue() = rxPrefs.getFloat(keys.customBrightnessValue, 0f)
val defaultViewer: Int
get() = prefs.getInt(getKey(R.string.pref_default_viewer_key), 1)
fun defaultViewer() = prefs.getInt(keys.defaultViewer, 1)
fun imageScaleType(): Preference<Int> {
return rxPrefs.getInteger(getKey(R.string.pref_image_scale_type_key), 1)
}
fun imageScaleType() = rxPrefs.getInteger(keys.imageScaleType, 1)
fun imageDecoder(): Preference<Int> {
return rxPrefs.getInteger(getKey(R.string.pref_image_decoder_key), 0)
}
fun imageDecoder() = rxPrefs.getInteger(keys.imageDecoder, 0)
fun zoomStart(): Preference<Int> {
return rxPrefs.getInteger(getKey(R.string.pref_zoom_start_key), 1)
}
fun zoomStart() = rxPrefs.getInteger(keys.zoomStart, 1)
fun readerTheme(): Preference<Int> {
return rxPrefs.getInteger(getKey(R.string.pref_reader_theme_key), 0)
}
fun readerTheme() = rxPrefs.getInteger(keys.readerTheme, 0)
fun portraitColumns(): Preference<Int> {
return rxPrefs.getInteger(getKey(R.string.pref_library_columns_portrait_key), 0)
}
fun readWithTapping() = rxPrefs.getBoolean(keys.readWithTapping, true)
fun landscapeColumns(): Preference<Int> {
return rxPrefs.getInteger(getKey(R.string.pref_library_columns_landscape_key), 0)
}
fun readWithVolumeKeys() = rxPrefs.getBoolean(keys.readWithVolumeKeys, false)
fun updateOnlyNonCompleted(): Boolean {
return prefs.getBoolean(getKey(R.string.pref_update_only_non_completed_key), false)
}
fun reencodeImage() = prefs.getBoolean(keys.reencodeImage, false)
fun autoUpdateMangaSync(): Boolean {
return prefs.getBoolean(getKey(R.string.pref_auto_update_manga_sync_key), true)
}
fun portraitColumns() = rxPrefs.getInteger(keys.portraitColumns, 0)
fun askUpdateMangaSync(): Boolean {
return prefs.getBoolean(getKey(R.string.pref_ask_update_manga_sync_key), false)
}
fun landscapeColumns() = rxPrefs.getInteger(keys.landscapeColumns, 0)
fun lastUsedCatalogueSource(): Preference<Int> {
return rxPrefs.getInteger(getKey(R.string.pref_last_catalogue_source_key), -1)
}
fun updateOnlyNonCompleted() = prefs.getBoolean(keys.updateOnlyNonCompleted, false)
fun seamlessMode(): Boolean {
return prefs.getBoolean(getKey(R.string.pref_seamless_mode_key), true)
}
fun autoUpdateMangaSync() = prefs.getBoolean(keys.autoUpdateMangaSync, true)
fun catalogueAsList(): Preference<Boolean> {
return rxPrefs.getBoolean(getKey(R.string.pref_display_catalogue_as_list), false)
}
fun askUpdateMangaSync() = prefs.getBoolean(keys.askUpdateMangaSync, false)
fun enabledLanguages(): Preference<MutableSet<String>> {
return rxPrefs.getStringSet(getKey(R.string.pref_source_languages), setOf("EN"))
}
fun lastUsedCatalogueSource() = rxPrefs.getInteger(keys.lastUsedCatalogueSource, -1)
fun getSourceUsername(source: Source): String {
return prefs.getString(SOURCE_ACCOUNT_USERNAME + source.id, "")
}
fun lastUsedCategory() = rxPrefs.getInteger(keys.lastUsedCategory, 0)
fun getSourcePassword(source: Source): String {
return prefs.getString(SOURCE_ACCOUNT_PASSWORD + source.id, "")
}
fun lastVersionCode() = rxPrefs.getInteger("last_version_code", 0)
fun seamlessMode() = prefs.getBoolean(keys.seamlessMode, true)
fun catalogueAsList() = rxPrefs.getBoolean(keys.catalogueAsList, false)
fun enabledLanguages() = rxPrefs.getStringSet(keys.enabledLanguages, setOf("EN"))
fun sourceUsername(source: Source) = prefs.getString(keys.sourceUsername(source.id), "")
fun sourcePassword(source: Source) = prefs.getString(keys.sourcePassword(source.id), "")
fun setSourceCredentials(source: Source, username: String, password: String) {
prefs.edit()
.putString(SOURCE_ACCOUNT_USERNAME + source.id, username)
.putString(SOURCE_ACCOUNT_PASSWORD + source.id, password)
.putString(keys.sourceUsername(source.id), username)
.putString(keys.sourcePassword(source.id), password)
.apply()
}
fun getMangaSyncUsername(sync: MangaSyncService): String {
return prefs.getString(MANGASYNC_ACCOUNT_USERNAME + sync.id, "")
}
fun mangaSyncUsername(sync: MangaSyncService) = prefs.getString(keys.syncUsername(sync.id), "")
fun getMangaSyncPassword(sync: MangaSyncService): String {
return prefs.getString(MANGASYNC_ACCOUNT_PASSWORD + sync.id, "")
}
fun mangaSyncPassword(sync: MangaSyncService) = prefs.getString(keys.syncPassword(sync.id), "")
fun setMangaSyncCredentials(sync: MangaSyncService, username: String, password: String) {
prefs.edit()
.putString(MANGASYNC_ACCOUNT_USERNAME + sync.id, username)
.putString(MANGASYNC_ACCOUNT_PASSWORD + sync.id, password)
.putString(keys.syncUsername(sync.id), username)
.putString(keys.syncPassword(sync.id), password)
.apply()
}
var downloadsDirectory: String
get() = prefs.getString(getKey(R.string.pref_download_directory_key), defaultDownloadsDir.absolutePath)
set(path) = prefs.edit().putString(getKey(R.string.pref_download_directory_key), path).apply()
fun downloadsDirectory() = rxPrefs.getString(keys.downloadsDirectory, defaultDownloadsDir.absolutePath)
fun downloadThreads(): Preference<Int> {
return rxPrefs.getInteger(getKey(R.string.pref_download_slots_key), 1)
}
fun downloadThreads() = rxPrefs.getInteger(keys.downloadThreads, 1)
fun downloadOnlyOverWifi(): Boolean {
return prefs.getBoolean(getKey(R.string.pref_download_only_over_wifi_key), true)
}
fun downloadOnlyOverWifi() = prefs.getBoolean(keys.downloadOnlyOverWifi, true)
fun removeAfterRead(): Boolean {
return prefs.getBoolean(getKey(R.string.pref_remove_after_read_key), false)
}
fun removeAfterRead() = prefs.getBoolean(keys.removeAfterRead, false)
fun removeAfterReadPrevious(): Boolean {
return prefs.getBoolean(getKey(R.string.pref_remove_after_read_previous_key), false)
}
fun removeAfterReadPrevious() = prefs.getBoolean(keys.removeAfterReadPrevious, false)
fun removeAfterMarkedAsRead(): Boolean {
return prefs.getBoolean(getKey(R.string.pref_remove_after_marked_as_read_key), false)
}
fun removeAfterMarkedAsRead() = prefs.getBoolean(keys.removeAfterMarkedAsRead, false)
fun updateOnlyWhenCharging(): Boolean {
return prefs.getBoolean(getKey(R.string.pref_update_only_when_charging_key), false)
}
fun libraryUpdateInterval() = rxPrefs.getInteger(keys.libraryUpdateInterval, 0)
fun libraryUpdateInterval(): Preference<Int> {
return rxPrefs.getInteger(getKey(R.string.pref_library_update_interval_key), 0)
}
fun libraryUpdateRestriction() = prefs.getStringSet(keys.libraryUpdateRestriction, emptySet())
fun filterDownloaded(): Preference<Boolean> {
return rxPrefs.getBoolean(getKey(R.string.pref_filter_downloaded_key), false)
}
fun filterDownloaded() = rxPrefs.getBoolean(keys.filterDownloaded, false)
fun filterUnread(): Preference<Boolean> {
return rxPrefs.getBoolean(getKey(R.string.pref_filter_unread_key), false)
}
fun filterUnread() = rxPrefs.getBoolean(keys.filterUnread, false)
}

View File

@ -1,8 +1,8 @@
package eu.kanade.tachiyomi.data.source
class Language(val lang: String, val code: String)
class Language(val code: String, val lang: String)
val EN = Language("English", "EN")
val RU = Language("Russian", "RU")
val EN = Language("EN", "English")
val RU = Language("RU", "Russian")
fun getLanguages(): List<Language> = listOf(EN, RU)
fun getLanguages() = listOf(EN, RU)

View File

@ -0,0 +1,51 @@
package eu.kanade.tachiyomi.data.source
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.source.model.Page
import rx.Observable
/**
* A basic interface for creating a source. It could be an online source, a local source, etc...
*/
interface Source {
/**
* Id for the source. Must be unique.
*/
val id: Int
/**
* Name of the source.
*/
val name: String
/**
* Returns an observable with the updated details for a manga.
*
* @param manga the manga to update.
*/
fun fetchMangaDetails(manga: Manga): Observable<Manga>
/**
* Returns an observable with all the available chapters for a manga.
*
* @param manga the manga to update.
*/
fun fetchChapterList(manga: Manga): Observable<List<Chapter>>
/**
* Returns an observable with the list of pages a chapter has.
*
* @param chapter the chapter.
*/
fun fetchPageList(chapter: Chapter): Observable<List<Page>>
/**
* Returns an observable with the path of the image.
*
* @param page the page.
*/
fun fetchImage(page: Page): Observable<Page>
}

View File

@ -1,21 +1,22 @@
package eu.kanade.tachiyomi.data.source
import android.Manifest.permission.READ_EXTERNAL_STORAGE
import android.content.Context
import eu.kanade.tachiyomi.data.source.base.Source
import eu.kanade.tachiyomi.data.source.online.english.Batoto
import eu.kanade.tachiyomi.data.source.online.english.Kissmanga
import eu.kanade.tachiyomi.data.source.online.english.Mangafox
import eu.kanade.tachiyomi.data.source.online.english.Mangahere
import eu.kanade.tachiyomi.data.source.online.russian.Mangachan;
import eu.kanade.tachiyomi.data.source.online.russian.Mintmanga;
import eu.kanade.tachiyomi.data.source.online.russian.Readmanga;
import eu.kanade.tachiyomi.data.source.online.english.ReadMangaToday
import java.util.*
import android.os.Environment
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.source.online.OnlineSource
import eu.kanade.tachiyomi.data.source.online.YamlOnlineSource
import eu.kanade.tachiyomi.data.source.online.english.*
import eu.kanade.tachiyomi.data.source.online.russian.Mangachan
import eu.kanade.tachiyomi.data.source.online.russian.Mintmanga
import eu.kanade.tachiyomi.data.source.online.russian.Readmanga
import eu.kanade.tachiyomi.util.hasPermission
import org.yaml.snakeyaml.Yaml
import timber.log.Timber
import java.io.File
open class SourceManager(private val context: Context) {
val sourcesMap: HashMap<Int, Source>
val BATOTO = 1
val MANGAHERE = 2
val MANGAFOX = 3
@ -27,38 +28,45 @@ open class SourceManager(private val context: Context) {
val LAST_SOURCE = 8
init {
sourcesMap = createSourcesMap()
}
val sourcesMap = createSources()
open fun get(sourceKey: Int): Source? {
return sourcesMap[sourceKey]
}
private fun createSource(sourceKey: Int): Source? = when (sourceKey) {
BATOTO -> Batoto(context)
MANGAHERE -> Mangahere(context)
MANGAFOX -> Mangafox(context)
KISSMANGA -> Kissmanga(context)
READMANGA -> Readmanga(context)
MINTMANGA -> Mintmanga(context)
MANGACHAN -> Mangachan(context)
READMANGATODAY -> ReadMangaToday(context)
fun getOnlineSources() = sourcesMap.values.filterIsInstance(OnlineSource::class.java)
private fun createSource(id: Int): Source? = when (id) {
BATOTO -> Batoto(context, id)
KISSMANGA -> Kissmanga(context, id)
MANGAHERE -> Mangahere(context, id)
MANGAFOX -> Mangafox(context, id)
READMANGA -> Readmanga(context, id)
MINTMANGA -> Mintmanga(context, id)
MANGACHAN -> Mangachan(context, id)
READMANGATODAY -> Readmangatoday(context, id)
else -> null
}
private fun createSourcesMap(): HashMap<Int, Source> {
val map = HashMap<Int, Source>()
private fun createSources(): Map<Int, Source> = hashMapOf<Int, Source>().apply {
for (i in 1..LAST_SOURCE) {
val source = createSource(i)
if (source != null) {
source.id = i
map.put(i, source)
createSource(i)?.let { put(i, it) }
}
val parsersDir = File(Environment.getExternalStorageDirectory().absolutePath +
File.separator + context.getString(R.string.app_name), "parsers")
if (parsersDir.exists() && context.hasPermission(READ_EXTERNAL_STORAGE)) {
val yaml = Yaml()
for (file in parsersDir.listFiles().filter { it.extension == "yml" }) {
try {
val map = file.inputStream().use { yaml.loadAs(it, Map::class.java) }
YamlOnlineSource(context, map).let { put(it.id, it) }
} catch (e: Exception) {
Timber.e("Error loading source from file. Bad format?")
}
}
}
return map
}
fun getSources(): List<Source> = ArrayList(sourcesMap.values)
}

View File

@ -1,99 +0,0 @@
package eu.kanade.tachiyomi.data.source.base;
import org.jsoup.nodes.Document;
import java.util.List;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.source.Language;
import eu.kanade.tachiyomi.data.source.model.MangasPage;
import okhttp3.Headers;
import okhttp3.Response;
import rx.Observable;
public abstract class BaseSource {
private int id;
// Id of the source
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public abstract Language getLang();
// Name of the source to display
public abstract String getName();
// Name of the source to display with the language
public String getVisibleName() {
return getName() + " (" + getLang().getCode() + ")";
}
// Base url of the source, like: http://example.com
public abstract String getBaseUrl();
// True if the source requires a login
public abstract boolean isLoginRequired();
// Return the initial popular mangas URL
protected abstract String getInitialPopularMangasUrl();
// Return the initial search url given a query
protected abstract String getInitialSearchUrl(String query);
// Get the popular list of mangas from the source's parsed document
protected abstract List<Manga> parsePopularMangasFromHtml(Document parsedHtml);
// Get the next popular page URL or null if it's the last
protected abstract String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page);
// Get the searched list of mangas from the source's parsed document
protected abstract List<Manga> parseSearchFromHtml(Document parsedHtml);
// Get the next search page URL or null if it's the last
protected abstract String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query);
// Given the URL of a manga and the result of the request, return the details of the manga
protected abstract Manga parseHtmlToManga(String mangaUrl, String unparsedHtml);
// Given the result of the request to mangas' chapters, return a list of chapters
protected abstract List<Chapter> parseHtmlToChapters(String unparsedHtml);
// Given the result of the request to a chapter, return the list of URLs of the chapter
protected abstract List<String> parseHtmlToPageUrls(String unparsedHtml);
// Given the result of the request to a chapter's page, return the URL of the image of the page
protected abstract String parseHtmlToImageUrl(String unparsedHtml);
// Login related methods, shouldn't be overriden if the source doesn't require it
public Observable<Boolean> login(String username, String password) {
throw new UnsupportedOperationException("Not implemented");
}
public boolean isLogged() {
throw new UnsupportedOperationException("Not implemented");
}
protected boolean isAuthenticationSuccessful(Response response) {
throw new UnsupportedOperationException("Not implemented");
}
// Default headers, it can be overriden by children or just add new keys
protected Headers.Builder headersBuilder() {
Headers.Builder builder = new Headers.Builder();
builder.add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)");
return builder;
}
@Override
public String toString() {
return getVisibleName();
}
}

View File

@ -1,15 +0,0 @@
package eu.kanade.tachiyomi.data.source.base;
import android.content.Context;
public abstract class LoginSource extends Source {
public LoginSource(Context context) {
super(context);
}
@Override
public boolean isLoginRequired() {
return true;
}
}

View File

@ -1,230 +0,0 @@
package eu.kanade.tachiyomi.data.source.base
import android.content.Context
import com.bumptech.glide.load.model.LazyHeaders
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.network.NetworkHelper
import eu.kanade.tachiyomi.data.network.get
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.source.model.MangasPage
import eu.kanade.tachiyomi.data.source.model.Page
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import rx.Observable
import rx.schedulers.Schedulers
import java.util.*
import javax.inject.Inject
abstract class Source(context: Context) : BaseSource() {
@Inject protected lateinit var networkService: NetworkHelper
@Inject protected lateinit var chapterCache: ChapterCache
@Inject protected lateinit var prefs: PreferencesHelper
val requestHeaders by lazy { headersBuilder().build() }
val glideHeaders by lazy { glideHeadersBuilder().build() }
init {
App.get(context).component.inject(this)
}
override fun isLoginRequired(): Boolean {
return false
}
protected fun popularMangaRequest(page: MangasPage): Request {
if (page.page == 1) {
page.url = initialPopularMangasUrl
}
return get(page.url, requestHeaders)
}
protected open fun searchMangaRequest(page: MangasPage, query: String): Request {
if (page.page == 1) {
page.url = getInitialSearchUrl(query)
}
return get(page.url, requestHeaders)
}
protected open fun mangaDetailsRequest(mangaUrl: String): Request {
return get(baseUrl + mangaUrl, requestHeaders)
}
protected fun chapterListRequest(mangaUrl: String): Request {
return get(baseUrl + mangaUrl, requestHeaders)
}
protected open fun pageListRequest(chapterUrl: String): Request {
return get(baseUrl + chapterUrl, requestHeaders)
}
protected open fun imageUrlRequest(page: Page): Request {
return get(page.url, requestHeaders)
}
protected open fun imageRequest(page: Page): Request {
return get(page.imageUrl, requestHeaders)
}
// Get the most popular mangas from the source
open fun pullPopularMangasFromNetwork(page: MangasPage): Observable<MangasPage> {
return networkService.requestBody(popularMangaRequest(page), true)
.map { Jsoup.parse(it) }
.doOnNext { doc -> page.mangas = parsePopularMangasFromHtml(doc) }
.doOnNext { doc -> page.nextPageUrl = parseNextPopularMangasUrl(doc, page) }
.map { response -> page }
}
// Get mangas from the source with a query
open fun searchMangasFromNetwork(page: MangasPage, query: String): Observable<MangasPage> {
return networkService.requestBody(searchMangaRequest(page, query), true)
.map { Jsoup.parse(it) }
.doOnNext { doc -> page.mangas = parseSearchFromHtml(doc) }
.doOnNext { doc -> page.nextPageUrl = parseNextSearchUrl(doc, page, query) }
.map { response -> page }
}
// Get manga details from the source
open fun pullMangaFromNetwork(mangaUrl: String): Observable<Manga> {
return networkService.requestBody(mangaDetailsRequest(mangaUrl))
.flatMap { Observable.just(parseHtmlToManga(mangaUrl, it)) }
}
// Get chapter list of a manga from the source
open fun pullChaptersFromNetwork(mangaUrl: String): Observable<List<Chapter>> {
return networkService.requestBody(chapterListRequest(mangaUrl))
.flatMap { unparsedHtml ->
val chapters = parseHtmlToChapters(unparsedHtml)
if (!chapters.isEmpty())
Observable.just(chapters)
else
Observable.error(Exception("No chapters found"))
}
}
open fun getCachedPageListOrPullFromNetwork(chapterUrl: String): Observable<List<Page>> {
return chapterCache.getPageListFromCache(getChapterCacheKey(chapterUrl))
.onErrorResumeNext { pullPageListFromNetwork(chapterUrl) }
.onBackpressureBuffer()
}
open fun pullPageListFromNetwork(chapterUrl: String): Observable<List<Page>> {
return networkService.requestBody(pageListRequest(chapterUrl))
.flatMap { unparsedHtml ->
val pages = convertToPages(parseHtmlToPageUrls(unparsedHtml))
if (!pages.isEmpty())
Observable.just(parseFirstPage(pages, unparsedHtml))
else
Observable.error(Exception("Page list is empty"))
}
}
open fun getAllImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
return Observable.from(pages)
.filter { page -> page.imageUrl != null }
.mergeWith(getRemainingImageUrlsFromPageList(pages))
}
// Get the URLs of the images of a chapter
open fun getRemainingImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
return Observable.from(pages)
.filter { page -> page.imageUrl == null }
.concatMap { getImageUrlFromPage(it) }
}
open fun getImageUrlFromPage(page: Page): Observable<Page> {
page.status = Page.LOAD_PAGE
return networkService.requestBody(imageUrlRequest(page))
.flatMap { unparsedHtml -> Observable.just(parseHtmlToImageUrl(unparsedHtml)) }
.onErrorResumeNext { e ->
page.status = Page.ERROR
Observable.just<String>(null)
}
.flatMap { imageUrl ->
page.imageUrl = imageUrl
Observable.just(page)
}
.subscribeOn(Schedulers.io())
}
open fun getCachedImage(page: Page): Observable<Page> {
val pageObservable = Observable.just(page)
if (page.imageUrl == null)
return pageObservable
return pageObservable
.flatMap { p ->
if (!chapterCache.isImageInCache(page.imageUrl)) {
return@flatMap cacheImage(page)
}
Observable.just(page)
}
.flatMap { p ->
page.imagePath = chapterCache.getImagePath(page.imageUrl)
page.status = Page.READY
Observable.just(page)
}
.onErrorResumeNext { e ->
page.status = Page.ERROR
Observable.just(page)
}
}
private fun cacheImage(page: Page): Observable<Page> {
page.status = Page.DOWNLOAD_IMAGE
return getImageProgressResponse(page)
.flatMap { resp ->
chapterCache.putImageToCache(page.imageUrl, resp)
Observable.just(page)
}
}
open fun getImageProgressResponse(page: Page): Observable<Response> {
return networkService.requestBodyProgress(imageRequest(page), page)
}
fun savePageList(chapterUrl: String, pages: List<Page>?) {
if (pages != null)
chapterCache.putPageListToCache(getChapterCacheKey(chapterUrl), pages)
}
protected open fun convertToPages(pageUrls: List<String>): List<Page> {
val pages = ArrayList<Page>()
for (i in pageUrls.indices) {
pages.add(Page(i, pageUrls[i]))
}
return pages
}
protected open fun parseFirstPage(pages: List<Page>, unparsedHtml: String): List<Page> {
val firstImage = parseHtmlToImageUrl(unparsedHtml)
pages[0].imageUrl = firstImage
return pages
}
protected fun getChapterCacheKey(chapterUrl: String): String {
return "$id$chapterUrl"
}
// Overridable method to allow custom parsing.
open fun parseChapterNumber(chapter: Chapter) {
}
protected open fun glideHeadersBuilder(): LazyHeaders.Builder {
val builder = LazyHeaders.Builder()
for ((key, value) in requestHeaders.toMultimap()) {
builder.addHeader(key, value[0])
}
return builder
}
}

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.data.source.model;
import java.io.Serializable;
import java.util.List;
import eu.kanade.tachiyomi.data.database.models.Chapter;
@ -25,6 +24,14 @@ public class Page implements ProgressListener {
public static final int READY = 3;
public static final int ERROR = 4;
public Page(int pageNumber, String url) {
this(pageNumber, url, null, null);
}
public Page(int pageNumber, String url, String imageUrl) {
this(pageNumber, url, imageUrl, null);
}
public Page(int pageNumber, String url, String imageUrl, String imagePath) {
this.pageNumber = pageNumber;
this.url = url;
@ -32,10 +39,6 @@ public class Page implements ProgressListener {
this.imagePath = imagePath;
}
public Page(int pageNumber, String url) {
this(pageNumber, url, null, null);
}
public int getPageNumber() {
return pageNumber;
}

View File

@ -0,0 +1,15 @@
package eu.kanade.tachiyomi.data.source.online
import eu.kanade.tachiyomi.data.source.Source
import okhttp3.Response
import rx.Observable
interface LoginSource : Source {
fun isLogged(): Boolean
fun login(username: String, password: String): Observable<Boolean>
fun isAuthenticationSuccessful(response: Response): Boolean
}

View File

@ -0,0 +1,449 @@
package eu.kanade.tachiyomi.data.source.online
import android.content.Context
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.network.GET
import eu.kanade.tachiyomi.data.network.NetworkHelper
import eu.kanade.tachiyomi.data.network.asObservable
import eu.kanade.tachiyomi.data.network.newCallWithProgress
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.source.Language
import eu.kanade.tachiyomi.data.source.Source
import eu.kanade.tachiyomi.data.source.model.MangasPage
import eu.kanade.tachiyomi.data.source.model.Page
import okhttp3.*
import rx.Observable
import javax.inject.Inject
/**
* A simple implementation for sources from a website.
*
* @param context the application context.
*/
abstract class OnlineSource(context: Context) : Source {
/**
* Network service.
*/
@Inject lateinit var network: NetworkHelper
/**
* Chapter cache.
*/
@Inject lateinit var chapterCache: ChapterCache
/**
* Preferences helper.
*/
@Inject lateinit var preferences: PreferencesHelper
/**
* Base url of the website without the trailing slash, like: http://mysite.com
*/
abstract val baseUrl: String
/**
* Language of the source.
*/
abstract val lang: Language
/**
* Headers used for requests.
*/
val headers by lazy { headersBuilder().build() }
/**
* Default network client for doing requests.
*/
open val client: OkHttpClient
get() = network.client
init {
// Inject dependencies.
App.get(context).component.inject(this)
}
/**
* Headers builder for requests. Implementations can override this method for custom headers.
*/
open protected fun headersBuilder() = Headers.Builder().apply {
add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)")
}
/**
* Visible name of the source.
*/
override fun toString() = "$name (${lang.code})"
/**
* Returns an observable containing a page with a list of manga. Normally it's not needed to
* override this method.
*
* @param page the page object where the information will be saved, like the list of manga,
* the current page and the next page url.
*/
open fun fetchPopularManga(page: MangasPage): Observable<MangasPage> = client
.newCall(popularMangaRequest(page))
.asObservable()
.map { response ->
page.apply {
mangas = mutableListOf<Manga>()
popularMangaParse(response, this)
}
}
/**
* Returns the request for the popular manga given the page. Override only if it's needed to
* send different headers or request method like POST.
*
* @param page the page object.
*/
open protected fun popularMangaRequest(page: MangasPage): Request {
if (page.page == 1) {
page.url = popularMangaInitialUrl()
}
return GET(page.url, headers)
}
/**
* Returns the absolute url of the first page to popular manga.
*/
abstract protected fun popularMangaInitialUrl(): String
/**
* Parse the response from the site. It should add a list of manga and the absolute url to the
* next page (if it has a next one) to [page].
*
* @param response the response from the site.
* @param page the page object to be filled.
*/
abstract protected fun popularMangaParse(response: Response, page: MangasPage)
/**
* Returns an observable containing a page with a list of manga. Normally it's not needed to
* override this method.
*
* @param page the page object where the information will be saved, like the list of manga,
* the current page and the next page url.
* @param query the search query.
*/
open fun fetchSearchManga(page: MangasPage, query: String): Observable<MangasPage> = client
.newCall(searchMangaRequest(page, query))
.asObservable()
.map { response ->
page.apply {
mangas = mutableListOf<Manga>()
searchMangaParse(response, this, query)
}
}
/**
* Returns the request for the search manga given the page. Override only if it's needed to
* send different headers or request method like POST.
*
* @param page the page object.
* @param query the search query.
*/
open protected fun searchMangaRequest(page: MangasPage, query: String): Request {
if (page.page == 1) {
page.url = searchMangaInitialUrl(query)
}
return GET(page.url, headers)
}
/**
* Returns the absolute url of the first page to popular manga.
*
* @param query the search query.
*/
abstract protected fun searchMangaInitialUrl(query: String): String
/**
* Parse the response from the site. It should add a list of manga and the absolute url to the
* next page (if it has a next one) to [page].
*
* @param response the response from the site.
* @param page the page object to be filled.
* @param query the search query.
*/
abstract protected fun searchMangaParse(response: Response, page: MangasPage, query: String)
/**
* Returns an observable with the updated details for a manga. Normally it's not needed to
* override this method.
*
* @param manga the manga to be updated.
*/
override fun fetchMangaDetails(manga: Manga): Observable<Manga> = client
.newCall(mangaDetailsRequest(manga))
.asObservable()
.map { response ->
Manga.create(manga.url, id).apply {
mangaDetailsParse(response, this)
initialized = true
}
}
/**
* Returns the request for updating a manga. Override only if it's needed to override the url,
* send different headers or request method like POST.
*
* @param manga the manga to be updated.
*/
open protected fun mangaDetailsRequest(manga: Manga): Request {
return GET(baseUrl + manga.url, headers)
}
/**
* Parse the response from the site. It should fill [manga].
*
* @param response the response from the site.
* @param manga the manga whose fields have to be filled.
*/
abstract protected fun mangaDetailsParse(response: Response, manga: Manga)
/**
* Returns an observable with the updated chapter list for a manga. Normally it's not needed to
* override this method.
*
* @param manga the manga to look for chapters.
*/
override fun fetchChapterList(manga: Manga): Observable<List<Chapter>> = client
.newCall(chapterListRequest(manga))
.asObservable()
.map { response ->
mutableListOf<Chapter>().apply {
chapterListParse(response, this)
if (isEmpty()) {
throw Exception("No chapters found")
}
}
}
/**
* Returns the request for updating the chapter list. Override only if it's needed to override
* the url, send different headers or request method like POST.
*
* @param manga the manga to look for chapters.
*/
open protected fun chapterListRequest(manga: Manga): Request {
return GET(baseUrl + manga.url, headers)
}
/**
* Parse the response from the site. It should fill [chapters].
*
* @param response the response from the site.
* @param chapters the chapter list to be filled.
*/
abstract protected fun chapterListParse(response: Response, chapters: MutableList<Chapter>)
/**
* Returns an observable with the page list for a chapter. It tries to return the page list from
* the local cache, otherwise fallbacks to network calling [fetchPageListFromNetwork].
*
* @param chapter the chapter whose page list has to be fetched.
*/
final override fun fetchPageList(chapter: Chapter): Observable<List<Page>> = chapterCache
.getPageListFromCache(getChapterCacheKey(chapter))
.onErrorResumeNext { fetchPageListFromNetwork(chapter) }
/**
* Returns an observable with the page list for a chapter. Normally it's not needed to override
* this method.
*
* @param chapter the chapter whose page list has to be fetched.
*/
open fun fetchPageListFromNetwork(chapter: Chapter): Observable<List<Page>> = client
.newCall(pageListRequest(chapter))
.asObservable()
.map { response ->
if (!response.isSuccessful) {
throw Exception("Webpage sent ${response.code()} code")
}
mutableListOf<Page>().apply {
pageListParse(response, this)
if (isEmpty()) {
throw Exception("Page list is empty")
}
}
}
/**
* Returns the request for getting the page list. Override only if it's needed to override the
* url, send different headers or request method like POST.
*
* @param chapter the chapter whose page list has to be fetched
*/
open protected fun pageListRequest(chapter: Chapter): Request {
return GET(baseUrl + chapter.url, headers)
}
/**
* Parse the response from the site. It should fill [pages].
*
* @param response the response from the site.
* @param pages the page list to be filled.
*/
abstract protected fun pageListParse(response: Response, pages: MutableList<Page>)
/**
* Returns the key for the page list to be stored in [ChapterCache].
*/
private fun getChapterCacheKey(chapter: Chapter) = "$id${chapter.url}"
/**
* Returns an observable with the page containing the source url of the image. If there's any
* error, it will return null instead of throwing an exception.
*
* @param page the page whose source image has to be fetched.
*/
open protected fun fetchImageUrl(page: Page): Observable<Page> {
page.status = Page.LOAD_PAGE
return client
.newCall(imageUrlRequest(page))
.asObservable()
.map { imageUrlParse(it) }
.doOnError { page.status = Page.ERROR }
.onErrorReturn { null }
.doOnNext { page.imageUrl = it }
.map { page }
}
/**
* Returns the request for getting the url to the source image. Override only if it's needed to
* override the url, send different headers or request method like POST.
*
* @param page the chapter whose page list has to be fetched
*/
open protected fun imageUrlRequest(page: Page): Request {
return GET(page.url, headers)
}
/**
* Parse the response from the site. It should return the absolute url to the source image.
*
* @param response the response from the site.
*/
abstract protected fun imageUrlParse(response: Response): String
/**
* Returns an observable of the page with the downloaded image.
*
* @param page the page whose source image has to be downloaded.
*/
final override fun fetchImage(page: Page): Observable<Page> =
if (page.imageUrl.isNullOrEmpty())
fetchImageUrl(page).flatMap { getCachedImage(it) }
else
getCachedImage(page)
/**
* Returns an observable with the response of the source image.
*
* @param page the page whose source image has to be downloaded.
*/
fun imageResponse(page: Page): Observable<Response> = client
.newCallWithProgress(imageRequest(page), page)
.asObservable()
.doOnNext {
if (!it.isSuccessful) {
it.close()
throw RuntimeException("Not a valid response")
}
}
/**
* Returns the request for getting the source image. Override only if it's needed to override
* the url, send different headers or request method like POST.
*
* @param page the chapter whose page list has to be fetched
*/
open protected fun imageRequest(page: Page): Request {
return GET(page.imageUrl, headers)
}
/**
* Returns an observable of the page that gets the image from the chapter or fallbacks to
* network and copies it to the cache calling [cacheImage].
*
* @param page the page.
*/
fun getCachedImage(page: Page): Observable<Page> {
val pageObservable = Observable.just(page)
if (page.imageUrl.isNullOrEmpty())
return pageObservable
return pageObservable
.flatMap {
if (!chapterCache.isImageInCache(page.imageUrl)) {
cacheImage(page)
} else {
Observable.just(page)
}
}
.doOnNext {
page.imagePath = chapterCache.getImagePath(page.imageUrl)
page.status = Page.READY
}
.doOnError { page.status = Page.ERROR }
.onErrorReturn { page }
}
/**
* Returns an observable of the page that downloads the image to [ChapterCache].
*
* @param page the page.
*/
private fun cacheImage(page: Page): Observable<Page> {
page.status = Page.DOWNLOAD_IMAGE
return imageResponse(page)
.doOnNext { chapterCache.putImageToCache(page.imageUrl, it, preferences.reencodeImage()) }
.map { page }
}
// Utility methods
/**
* Returns an absolute url from a href.
*
* Ex:
* href="http://example.com/foo" url="http://example.com" -> http://example.com/foo
* href="/mypath" url="http://example.com/foo" -> http://example.com/mypath
* href="bar" url="http://example.com/foo" -> http://example.com/bar
* href="bar" url="http://example.com/foo/" -> http://example.com/foo/bar
*
* @param href the href attribute from the html.
* @param url the requested url.
*/
fun getAbsoluteUrl(href: String, url: HttpUrl) = when {
href.startsWith("http://") || href.startsWith("https://") -> href
href.startsWith("/") -> url.newBuilder().encodedPath("/").fragment(null).query(null)
.toString() + href.substring(1)
else -> url.toString().substringBeforeLast('/') + "/$href"
}
fun fetchAllImageUrlsFromPageList(pages: List<Page>) = Observable.from(pages)
.filter { !it.imageUrl.isNullOrEmpty() }
.mergeWith(fetchRemainingImageUrlsFromPageList(pages))
fun fetchRemainingImageUrlsFromPageList(pages: List<Page>) = Observable.from(pages)
.filter { it.imageUrl.isNullOrEmpty() }
.concatMap { fetchImageUrl(it) }
fun savePageList(chapter: Chapter, pages: List<Page>?) {
if (pages != null) {
chapterCache.putPageListToCache(getChapterCacheKey(chapter), pages)
}
}
// Overridable method to allow custom parsing.
open fun parseChapterNumber(chapter: Chapter) {
}
}

View File

@ -0,0 +1,189 @@
package eu.kanade.tachiyomi.data.source.online
import android.content.Context
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.source.model.MangasPage
import eu.kanade.tachiyomi.data.source.model.Page
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
/**
* A simple implementation for sources from a website using Jsoup, an HTML parser.
*
* @param context the application context.
*/
abstract class ParsedOnlineSource(context: Context) : OnlineSource(context) {
/**
* Parse the response from the site and fills [page].
*
* @param response the response from the site.
* @param page the page object to be filled.
*/
override fun popularMangaParse(response: Response, page: MangasPage) {
val document = Jsoup.parse(response.body().string())
for (element in document.select(popularMangaSelector())) {
Manga().apply {
source = this@ParsedOnlineSource.id
popularMangaFromElement(element, this)
page.mangas.add(this)
}
}
popularMangaNextPageSelector()?.let { selector ->
page.nextPageUrl = document.select(selector).first()?.attr("href")?.let {
getAbsoluteUrl(it, response.request().url())
}
}
}
/**
* Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
*/
abstract protected fun popularMangaSelector(): String
/**
* Fills [manga] with the given [element]. Most sites only show the title and the url, it's
* totally safe to fill only those two values.
*
* @param element an element obtained from [popularMangaSelector].
* @param manga the manga to fill.
*/
abstract protected fun popularMangaFromElement(element: Element, manga: Manga)
/**
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
* there's no next page.
*/
abstract protected fun popularMangaNextPageSelector(): String?
/**
* Parse the response from the site and fills [page].
*
* @param response the response from the site.
* @param page the page object to be filled.
* @param query the search query.
*/
override fun searchMangaParse(response: Response, page: MangasPage, query: String) {
val document = Jsoup.parse(response.body().string())
for (element in document.select(searchMangaSelector())) {
Manga().apply {
source = this@ParsedOnlineSource.id
searchMangaFromElement(element, this)
page.mangas.add(this)
}
}
searchMangaNextPageSelector()?.let { selector ->
page.nextPageUrl = document.select(selector).first()?.attr("href")?.let {
getAbsoluteUrl(it, response.request().url())
}
}
}
/**
* Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
*/
abstract protected fun searchMangaSelector(): String
/**
* Fills [manga] with the given [element]. Most sites only show the title and the url, it's
* totally safe to fill only those two values.
*
* @param element an element obtained from [searchMangaSelector].
* @param manga the manga to fill.
*/
abstract protected fun searchMangaFromElement(element: Element, manga: Manga)
/**
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
* there's no next page.
*/
abstract protected fun searchMangaNextPageSelector(): String?
/**
* Parse the response from the site and fills the details of [manga].
*
* @param response the response from the site.
* @param manga the manga to fill.
*/
override fun mangaDetailsParse(response: Response, manga: Manga) {
mangaDetailsParse(Jsoup.parse(response.body().string()), manga)
}
/**
* Fills the details of [manga] from the given [document].
*
* @param document the parsed document.
* @param manga the manga to fill.
*/
abstract protected fun mangaDetailsParse(document: Document, manga: Manga)
/**
* Parse the response from the site and fills the chapter list.
*
* @param response the response from the site.
* @param chapters the list of chapters to fill.
*/
override fun chapterListParse(response: Response, chapters: MutableList<Chapter>) {
val document = Jsoup.parse(response.body().string())
for (element in document.select(chapterListSelector())) {
Chapter.create().apply {
chapterFromElement(element, this)
chapters.add(this)
}
}
}
/**
* Returns the Jsoup selector that returns a list of [Element] corresponding to each chapter.
*/
abstract protected fun chapterListSelector(): String
/**
* Fills [chapter] with the given [element].
*
* @param element an element obtained from [chapterListSelector].
* @param chapter the chapter to fill.
*/
abstract protected fun chapterFromElement(element: Element, chapter: Chapter)
/**
* Parse the response from the site and fills the page list.
*
* @param response the response from the site.
* @param pages the list of pages to fill.
*/
override fun pageListParse(response: Response, pages: MutableList<Page>) {
pageListParse(Jsoup.parse(response.body().string()), pages)
}
/**
* Fills [pages] from the given [document].
*
* @param document the parsed document.
* @param pages the list of pages to fill.
*/
abstract protected fun pageListParse(document: Document, pages: MutableList<Page>)
/**
* Parse the response from the site and returns the absolute url to the source image.
*
* @param response the response from the site.
*/
override fun imageUrlParse(response: Response): String {
return imageUrlParse(Jsoup.parse(response.body().string()))
}
/**
* Returns the absolute url to the source image from the document.
*
* @param document the parsed document.
*/
abstract protected fun imageUrlParse(document: Document): String
}

View File

@ -0,0 +1,165 @@
package eu.kanade.tachiyomi.data.source.online
import android.content.Context
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.network.GET
import eu.kanade.tachiyomi.data.network.POST
import eu.kanade.tachiyomi.data.source.getLanguages
import eu.kanade.tachiyomi.data.source.model.MangasPage
import eu.kanade.tachiyomi.data.source.model.Page
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Element
import java.text.SimpleDateFormat
import java.util.*
class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(context) {
val map = YamlSourceNode(mappings)
override val name: String
get() = map.name
override val baseUrl = map.host.let {
if (it.endsWith("/")) it.dropLast(1) else it
}
override val lang = map.lang.toUpperCase().let { code ->
getLanguages().find { code == it.code }!!
}
override val client = when(map.client) {
"cloudflare" -> network.cloudflareClient
else -> network.client
}
override val id = map.id.let {
if (it is Int) it else (lang.code.hashCode() + 31 * it.hashCode()) and 0x7fffffff
}
override fun popularMangaRequest(page: MangasPage): Request {
if (page.page == 1) {
page.url = popularMangaInitialUrl()
}
return when (map.popular.method?.toLowerCase()) {
"post" -> POST(page.url, headers, map.popular.createForm())
else -> GET(page.url, headers)
}
}
override fun popularMangaInitialUrl() = map.popular.url
override fun popularMangaParse(response: Response, page: MangasPage) {
val document = Jsoup.parse(response.body().string())
for (element in document.select(map.popular.manga_css)) {
Manga().apply {
source = this@YamlOnlineSource.id
title = element.text()
setUrl(element.attr("href"))
page.mangas.add(this)
}
}
map.popular.next_url_css?.let { selector ->
page.nextPageUrl = document.select(selector).first()?.attr("href")?.let {
getAbsoluteUrl(it, response.request().url())
}
}
}
override fun searchMangaRequest(page: MangasPage, query: String): Request {
if (page.page == 1) {
page.url = searchMangaInitialUrl(query)
}
return when (map.search.method?.toLowerCase()) {
"post" -> POST(page.url, headers, map.search.createForm())
else -> GET(page.url, headers)
}
}
override fun searchMangaInitialUrl(query: String) = map.search.url.replace("\$query", query)
override fun searchMangaParse(response: Response, page: MangasPage, query: String) {
val document = Jsoup.parse(response.body().string())
for (element in document.select(map.search.manga_css)) {
Manga().apply {
source = this@YamlOnlineSource.id
title = element.text()
setUrl(element.attr("href"))
page.mangas.add(this)
}
}
map.search.next_url_css?.let { selector ->
page.nextPageUrl = document.select(selector).first()?.attr("href")?.let {
getAbsoluteUrl(it, response.request().url())
}
}
}
override fun mangaDetailsParse(response: Response, manga: Manga) {
val document = Jsoup.parse(response.body().string())
with(map.manga) {
val pool = parts.get(document)
manga.author = author?.process(document, pool)
manga.artist = artist?.process(document, pool)
manga.description = summary?.process(document, pool)
manga.thumbnail_url = cover?.process(document, pool)
manga.genre = genres?.process(document, pool)
manga.status = status?.getStatus(document, pool) ?: Manga.UNKNOWN
}
}
override fun chapterListParse(response: Response, chapters: MutableList<Chapter>) {
val document = Jsoup.parse(response.body().string())
with(map.chapters) {
val pool = emptyMap<String, Element>()
val dateFormat = SimpleDateFormat(date?.format, Locale.ENGLISH)
for (element in document.select(chapter_css)) {
val chapter = Chapter.create()
element.select(title).first().let {
chapter.name = it.text()
chapter.setUrl(it.attr("href"))
}
val dateElement = element.select(date?.select).first()
chapter.date_upload = date?.getDate(dateElement, pool, dateFormat)?.time ?: 0
chapters.add(chapter)
}
}
}
override fun pageListParse(response: Response, pages: MutableList<Page>) {
val document = Jsoup.parse(response.body().string())
with(map.pages) {
val url = response.request().url().toString()
pages_css?.let {
for (element in document.select(it)) {
val value = element.attr(pages_attr)
val pageUrl = replace?.let { url.replace(it.toRegex(), replacement!!.replace("\$value", value)) } ?: value
pages.add(Page(pages.size, pageUrl))
}
}
for ((i, element) in document.select(image_css).withIndex()) {
pages.getOrNull(i)?.imageUrl = element.attr(image_attr).let {
getAbsoluteUrl(it, response.request().url())
}
}
}
}
override fun imageUrlParse(response: Response): String {
val document = Jsoup.parse(response.body().string())
return with(map.pages) {
document.select(image_css).first().attr(image_attr).let {
getAbsoluteUrl(it, response.request().url())
}
}
}
}

View File

@ -0,0 +1,214 @@
@file:Suppress("UNCHECKED_CAST")
package eu.kanade.tachiyomi.data.source.online
import eu.kanade.tachiyomi.data.database.models.Manga
import okhttp3.FormBody
import okhttp3.RequestBody
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
private fun toMap(map: Any?) = map as? Map<String, Any?>
class YamlSourceNode(uncheckedMap: Map<*, *>) {
val map = toMap(uncheckedMap)!!
val id: Any by map
val name: String by map
val host: String by map
val lang: String by map
val client: String?
get() = map["client"] as? String
val popular = PopularNode(toMap(map["popular"])!!)
val search = SearchNode(toMap(map["search"])!!)
val manga = MangaNode(toMap(map["manga"])!!)
val chapters = ChaptersNode(toMap(map["chapters"])!!)
val pages = PagesNode(toMap(map["pages"])!!)
}
interface RequestableNode {
val map: Map<String, Any?>
val url: String
get() = map["url"] as String
val method: String?
get() = map["method"] as? String
val payload: Map<String, String>?
get() = map["payload"] as? Map<String, String>
fun createForm(): RequestBody {
return FormBody.Builder().apply {
payload?.let {
for ((key, value) in it) {
add(key, value)
}
}
}.build()
}
}
class PopularNode(override val map: Map<String, Any?>): RequestableNode {
val manga_css: String by map
val next_url_css: String?
get() = map["next_url_css"] as? String
}
class SearchNode(override val map: Map<String, Any?>): RequestableNode {
val manga_css: String by map
val next_url_css: String?
get() = map["next_url_css"] as? String
}
class MangaNode(private val map: Map<String, Any?>) {
val parts = CacheNode(toMap(map["parts"]) ?: emptyMap())
val artist = toMap(map["artist"])?.let { SelectableNode(it) }
val author = toMap(map["author"])?.let { SelectableNode(it) }
val summary = toMap(map["summary"])?.let { SelectableNode(it) }
val status = toMap(map["status"])?.let { StatusNode(it) }
val genres = toMap(map["genres"])?.let { SelectableNode(it) }
val cover = toMap(map["cover"])?.let { CoverNode(it) }
}
class ChaptersNode(private val map: Map<String, Any?>) {
val chapter_css: String by map
val title: String by map
val date = toMap(toMap(map["date"]))?.let { DateNode(it) }
}
class CacheNode(private val map: Map<String, Any?>) {
fun get(document: Document) = map.mapValues { document.select(it.value as String).first() }
}
open class SelectableNode(private val map: Map<String, Any?>) {
val select: String by map
val from: String?
get() = map["from"] as? String
open val attr: String?
get() = map["attr"] as? String
val capture: String?
get() = map["capture"] as? String
fun process(document: Element, cache: Map<String, Element>): String {
val parent = from?.let { cache[it] } ?: document
val node = parent.select(select).first()
var text = attr?.let { node.attr(it) } ?: node.text()
capture?.let {
text = Regex(it).find(text)?.groupValues?.get(1) ?: text
}
return text
}
}
class StatusNode(private val map: Map<String, Any?>) : SelectableNode(map) {
val complete: String?
get() = map["complete"] as? String
val ongoing: String?
get() = map["ongoing"] as? String
val licensed: String?
get() = map["licensed"] as? String
fun getStatus(document: Element, cache: Map<String, Element>): Int {
val text = process(document, cache)
complete?.let {
if (text.contains(it)) return Manga.COMPLETED
}
ongoing?.let {
if (text.contains(it)) return Manga.ONGOING
}
licensed?.let {
if (text.contains(it)) return Manga.LICENSED
}
return Manga.UNKNOWN
}
}
class CoverNode(private val map: Map<String, Any?>) : SelectableNode(map) {
override val attr: String?
get() = map["attr"] as? String ?: "src"
}
class DateNode(private val map: Map<String, Any?>) : SelectableNode(map) {
val format: String by map
fun getDate(document: Element, cache: Map<String, Element>, formatter: SimpleDateFormat): Date {
val text = process(document, cache)
try {
return formatter.parse(text)
} catch (exception: ParseException) {}
for (i in 0..7) {
(map["day$i"] as? List<String>)?.let {
it.find { it.toRegex().containsMatchIn(text) }?.let {
return Calendar.getInstance().apply { add(Calendar.DATE, -i) }.time
}
}
}
return Date(0)
}
}
class PagesNode(private val map: Map<String, Any?>) {
val pages_css: String?
get() = map["pages_css"] as? String
val pages_attr: String?
get() = map["pages_attr"] as? String ?: "value"
val replace: String?
get() = map["url_replace"] as? String
val replacement: String?
get() = map["url_replacement"] as? String
val image_css: String by map
val image_attr: String
get() = map["image_attr"] as? String ?: "src"
}

View File

@ -1,393 +0,0 @@
package eu.kanade.tachiyomi.data.source.online.english;
import android.content.Context;
import android.net.Uri;
import android.text.Html;
import android.text.TextUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.net.HttpCookie;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.network.ReqKt;
import eu.kanade.tachiyomi.data.source.Language;
import eu.kanade.tachiyomi.data.source.LanguageKt;
import eu.kanade.tachiyomi.data.source.base.LoginSource;
import eu.kanade.tachiyomi.data.source.model.MangasPage;
import eu.kanade.tachiyomi.data.source.model.Page;
import eu.kanade.tachiyomi.util.Parser;
import okhttp3.FormBody;
import okhttp3.Headers;
import okhttp3.Request;
import okhttp3.Response;
import rx.Observable;
import rx.functions.Func1;
public class Batoto extends LoginSource {
public static final String NAME = "Batoto";
public static final String BASE_URL = "http://bato.to";
public static final String POPULAR_MANGAS_URL = BASE_URL + "/search_ajax?order_cond=views&order=desc&p=%s";
public static final String SEARCH_URL = BASE_URL + "/search_ajax?name=%s&p=%s";
public static final String CHAPTER_URL = BASE_URL + "/areader?id=%s&p=1";
public static final String PAGE_URL = BASE_URL + "/areader?id=%s&p=%s";
public static final String MANGA_URL = BASE_URL + "/comic_pop?id=%s";
public static final String LOGIN_URL = BASE_URL + "/forums/index.php?app=core&module=global&section=login";
public static final Pattern staffNotice = Pattern.compile("=+Batoto Staff Notice=+([^=]+)==+", Pattern.CASE_INSENSITIVE);
private Pattern datePattern;
private Map<String, Integer> dateFields;
public Batoto(Context context) {
super(context);
datePattern = Pattern.compile("(\\d+|A|An)\\s+(.*?)s? ago.*");
dateFields = new HashMap<String, Integer>() {{
put("second", Calendar.SECOND);
put("minute", Calendar.MINUTE);
put("hour", Calendar.HOUR);
put("day", Calendar.DATE);
put("week", Calendar.WEEK_OF_YEAR);
put("month", Calendar.MONTH);
put("year", Calendar.YEAR);
}};
}
@Override
public String getName() {
return NAME;
}
@Override
public String getBaseUrl() {
return BASE_URL;
}
public Language getLang() {
return LanguageKt.getEN();
}
@Override
protected Headers.Builder headersBuilder() {
Headers.Builder builder = super.headersBuilder();
builder.add("Cookie", "lang_option=English");
builder.add("Referer", "http://bato.to/reader");
return builder;
}
@Override
public String getInitialPopularMangasUrl() {
return String.format(POPULAR_MANGAS_URL, 1);
}
@Override
public String getInitialSearchUrl(String query) {
return String.format(SEARCH_URL, Uri.encode(query), 1);
}
@Override
protected Request mangaDetailsRequest(String mangaUrl) {
String mangaId = mangaUrl.substring(mangaUrl.lastIndexOf("r") + 1);
return ReqKt.get(String.format(MANGA_URL, mangaId), getRequestHeaders());
}
@Override
protected Request pageListRequest(String pageUrl) {
String id = pageUrl.substring(pageUrl.indexOf("#") + 1);
return ReqKt.get(String.format(CHAPTER_URL, id), getRequestHeaders());
}
@Override
protected Request imageUrlRequest(Page page) {
String pageUrl = page.getUrl();
int start = pageUrl.indexOf("#") + 1;
int end = pageUrl.indexOf("_", start);
String id = pageUrl.substring(start, end);
return ReqKt.get(String.format(PAGE_URL, id, pageUrl.substring(end+1)), getRequestHeaders());
}
private List<Manga> parseMangasFromHtml(Document parsedHtml) {
List<Manga> mangaList = new ArrayList<>();
if (!parsedHtml.text().contains("No (more) comics found!")) {
for (Element currentHtmlBlock : parsedHtml.select("tr:not([id]):not([class])")) {
Manga manga = constructMangaFromHtmlBlock(currentHtmlBlock);
mangaList.add(manga);
}
}
return mangaList;
}
@Override
protected List<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
return parseMangasFromHtml(parsedHtml);
}
@Override
protected String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page) {
Element next = Parser.element(parsedHtml, "#show_more_row");
return next != null ? String.format(POPULAR_MANGAS_URL, page.page + 1) : null;
}
@Override
protected List<Manga> parseSearchFromHtml(Document parsedHtml) {
return parseMangasFromHtml(parsedHtml);
}
private Manga constructMangaFromHtmlBlock(Element htmlBlock) {
Manga manga = new Manga();
manga.source = getId();
Element urlElement = Parser.element(htmlBlock, "a[href^=http://bato.to]");
if (urlElement != null) {
manga.setUrl(urlElement.attr("href"));
manga.title = urlElement.text().trim();
}
return manga;
}
@Override
protected String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query) {
Element next = Parser.element(parsedHtml, "#show_more_row");
return next != null ? String.format(SEARCH_URL, query, page.page + 1) : null;
}
@Override
protected Manga parseHtmlToManga(String mangaUrl, String unparsedHtml) {
Document parsedDocument = Jsoup.parse(unparsedHtml);
Element tbody = parsedDocument.select("tbody").first();
Element artistElement = tbody.select("tr:contains(Author/Artist:)").first();
Elements genreElements = tbody.select("tr:contains(Genres:) img");
Manga manga = Manga.create(mangaUrl);
manga.author = Parser.text(artistElement, "td:eq(1)");
manga.artist = Parser.text(artistElement, "td:eq(2)", manga.author);
manga.description = Parser.text(tbody, "tr:contains(Description:) > td:eq(1)");
manga.thumbnail_url = Parser.src(parsedDocument, "img[src^=http://img.bato.to/forums/uploads/]");
manga.status = parseStatus(Parser.text(parsedDocument, "tr:contains(Status:) > td:eq(1)"));
if (!genreElements.isEmpty()) {
List<String> genres = new ArrayList<>();
for (Element element : genreElements) {
genres.add(element.attr("alt"));
}
manga.genre = TextUtils.join(", ", genres);
}
manga.initialized = true;
return manga;
}
private int parseStatus(String status) {
switch (status) {
case "Ongoing":
return Manga.ONGOING;
case "Complete":
return Manga.COMPLETED;
default:
return Manga.UNKNOWN;
}
}
@Override
protected List<Chapter> parseHtmlToChapters(String unparsedHtml) {
Matcher matcher = staffNotice.matcher(unparsedHtml);
if (matcher.find()) {
String notice = Html.fromHtml(matcher.group(1)).toString().trim();
throw new RuntimeException(notice);
}
Document parsedDocument = Jsoup.parse(unparsedHtml);
List<Chapter> chapterList = new ArrayList<>();
Elements chapterElements = parsedDocument.select("tr.row.lang_English.chapter_row");
for (Element chapterElement : chapterElements) {
Chapter chapter = constructChapterFromHtmlBlock(chapterElement);
chapterList.add(chapter);
}
return chapterList;
}
private Chapter constructChapterFromHtmlBlock(Element chapterElement) {
Chapter chapter = Chapter.create();
Element urlElement = chapterElement.select("a[href^=http://bato.to/reader").first();
Element dateElement = chapterElement.select("td").get(4);
if (urlElement != null) {
String fieldUrl = urlElement.attr("href");
chapter.setUrl(fieldUrl);
chapter.name = urlElement.text().trim();
}
if (dateElement != null) {
chapter.date_upload = parseDateFromElement(dateElement);
}
return chapter;
}
@SuppressWarnings("WrongConstant")
private long parseDateFromElement(Element dateElement) {
String dateAsString = dateElement.text();
Date date;
try {
date = new SimpleDateFormat("dd MMMMM yyyy - hh:mm a", Locale.ENGLISH).parse(dateAsString);
} catch (ParseException e) {
Matcher m = datePattern.matcher(dateAsString);
if (m.matches()) {
String number = m.group(1);
int amount = number.contains("A") ? 1 : Integer.parseInt(m.group(1));
String unit = m.group(2);
Calendar cal = Calendar.getInstance();
cal.add(dateFields.get(unit), -amount);
date = cal.getTime();
} else {
return 0;
}
}
return date.getTime();
}
@Override
protected List<String> parseHtmlToPageUrls(String unparsedHtml) {
Document parsedDocument = Jsoup.parse(unparsedHtml);
List<String> pageUrlList = new ArrayList<>();
Element selectElement = Parser.element(parsedDocument, "#page_select");
if (selectElement != null) {
for (Element pageUrlElement : selectElement.select("option")) {
pageUrlList.add(pageUrlElement.attr("value"));
}
} else {
// For webtoons in one page
for (int i = 0; i < parsedDocument.select("div > img").size(); i++) {
pageUrlList.add("");
}
}
return pageUrlList;
}
@Override
protected List<Page> parseFirstPage(List<? extends Page> pages, String unparsedHtml) {
if (!unparsedHtml.contains("Want to see this chapter per page instead?")) {
String firstImage = parseHtmlToImageUrl(unparsedHtml);
pages.get(0).setImageUrl(firstImage);
} else {
// For webtoons in one page
Document parsedDocument = Jsoup.parse(unparsedHtml);
Elements imageUrls = parsedDocument.select("div > img");
for (int i = 0; i < pages.size(); i++) {
pages.get(i).setImageUrl(imageUrls.get(i).attr("src"));
}
}
return (List<Page>) pages;
}
@Override
protected String parseHtmlToImageUrl(String unparsedHtml) {
int beginIndex = unparsedHtml.indexOf("<img id=\"comic_page\"");
int endIndex = unparsedHtml.indexOf("</a>", beginIndex);
String trimmedHtml = unparsedHtml.substring(beginIndex, endIndex);
Document parsedDocument = Jsoup.parse(trimmedHtml);
Element imageElement = parsedDocument.getElementById("comic_page");
return imageElement.attr("src");
}
@Override
public Observable<Boolean> login(final String username, final String password) {
return getNetworkService().requestBody(ReqKt.get(LOGIN_URL, getRequestHeaders()))
.flatMap(new Func1<String, Observable<Response>>() {
@Override
public Observable<Response> call(String response) {return doLogin(response, username, password);}
})
.map(new Func1<Response, Boolean>() {
@Override
public Boolean call(Response resp) {return isAuthenticationSuccessful(resp);}
});
}
private Observable<Response> doLogin(String response, String username, String password) {
Document doc = Jsoup.parse(response);
Element form = doc.select("#login").first();
String postUrl = form.attr("action");
FormBody.Builder formBody = new FormBody.Builder();
Element authKey = form.select("input[name=auth_key]").first();
formBody.add(authKey.attr("name"), authKey.attr("value"));
formBody.add("ips_username", username);
formBody.add("ips_password", password);
formBody.add("invisible", "1");
formBody.add("rememberMe", "1");
return getNetworkService().request(ReqKt.post(postUrl, getRequestHeaders(), formBody.build()));
}
@Override
protected boolean isAuthenticationSuccessful(Response response) {
return response.priorResponse() != null && response.priorResponse().code() == 302;
}
@Override
public boolean isLogged() {
try {
for ( HttpCookie cookie : getNetworkService().getCookies().get(new URI(BASE_URL)) ) {
if (cookie.getName().equals("pass_hash"))
return true;
}
} catch (URISyntaxException e) {
e.printStackTrace();
}
return false;
}
@Override
public Observable<List<Chapter>> pullChaptersFromNetwork(final String mangaUrl) {
Observable<List<Chapter>> observable;
String username = getPrefs().getSourceUsername(this);
String password = getPrefs().getSourcePassword(this);
if (username.isEmpty() && password.isEmpty()) {
observable = Observable.error(new Exception("User not logged"));
}
else if (!isLogged()) {
observable = login(username, password)
.flatMap(new Func1<Boolean, Observable<? extends List<Chapter>>>() {
@Override
public Observable<? extends List<Chapter>> call(Boolean result) {return Batoto.super.pullChaptersFromNetwork(mangaUrl);}
});
}
else {
observable = super.pullChaptersFromNetwork(mangaUrl);
}
return observable;
}
}

View File

@ -0,0 +1,269 @@
package eu.kanade.tachiyomi.data.source.online.english
import android.content.Context
import android.net.Uri
import android.text.Html
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.network.GET
import eu.kanade.tachiyomi.data.network.POST
import eu.kanade.tachiyomi.data.network.asObservable
import eu.kanade.tachiyomi.data.source.EN
import eu.kanade.tachiyomi.data.source.Language
import eu.kanade.tachiyomi.data.source.model.MangasPage
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.data.source.online.LoginSource
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
import eu.kanade.tachiyomi.util.selectText
import okhttp3.FormBody
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import java.net.URI
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
import java.util.regex.Pattern
class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(context), LoginSource {
override val name = "Batoto"
override val baseUrl = "http://bato.to"
override val lang: Language get() = EN
private val datePattern = Pattern.compile("(\\d+|A|An)\\s+(.*?)s? ago.*")
private val dateFields = HashMap<String, Int>().apply {
put("second", Calendar.SECOND)
put("minute", Calendar.MINUTE)
put("hour", Calendar.HOUR)
put("day", Calendar.DATE)
put("week", Calendar.WEEK_OF_YEAR)
put("month", Calendar.MONTH)
put("year", Calendar.YEAR)
}
private val staffNotice = Pattern.compile("=+Batoto Staff Notice=+([^=]+)==+", Pattern.CASE_INSENSITIVE)
override fun headersBuilder() = super.headersBuilder()
.add("Cookie", "lang_option=English")
private val pageHeaders = super.headersBuilder()
.add("Referer", "http://bato.to/reader")
.build()
override fun popularMangaInitialUrl() = "$baseUrl/search_ajax?order_cond=views&order=desc&p=1"
override fun popularMangaParse(response: Response, page: MangasPage) {
val document = Jsoup.parse(response.body().string())
for (element in document.select(popularMangaSelector())) {
Manga().apply {
source = this@Batoto.id
popularMangaFromElement(element, this)
page.mangas.add(this)
}
}
page.nextPageUrl = document.select(popularMangaNextPageSelector()).first()?.let {
"$baseUrl/search_ajax?order_cond=views&order=desc&p=${page.page + 1}"
}
}
override fun popularMangaSelector() = "tr:not([id]):not([class])"
override fun popularMangaFromElement(element: Element, manga: Manga) {
element.select("a[href^=http://bato.to]").first().let {
manga.setUrl(it.attr("href"))
manga.title = it.text().trim()
}
}
override fun popularMangaNextPageSelector() = "#show_more_row"
override fun searchMangaInitialUrl(query: String) = "$baseUrl/search_ajax?name=${Uri.encode(query)}&p=1"
override fun searchMangaParse(response: Response, page: MangasPage, query: String) {
val document = Jsoup.parse(response.body().string())
for (element in document.select(searchMangaSelector())) {
Manga().apply {
source = this@Batoto.id
searchMangaFromElement(element, this)
page.mangas.add(this)
}
}
page.nextPageUrl = document.select(searchMangaNextPageSelector()).first()?.let {
"$baseUrl/search_ajax?name=${Uri.encode(query)}&p=${page.page + 1}"
}
}
override fun searchMangaSelector() = popularMangaSelector()
override fun searchMangaFromElement(element: Element, manga: Manga) {
popularMangaFromElement(element, manga)
}
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
override fun mangaDetailsRequest(manga: Manga): Request {
val mangaId = manga.url.substringAfterLast("r")
return GET("$baseUrl/comic_pop?id=$mangaId", headers)
}
override fun mangaDetailsParse(document: Document, manga: Manga) {
val tbody = document.select("tbody").first()
val artistElement = tbody.select("tr:contains(Author/Artist:)").first()
manga.author = artistElement.selectText("td:eq(1)")
manga.artist = artistElement.selectText("td:eq(2)") ?: manga.author
manga.description = tbody.selectText("tr:contains(Description:) > td:eq(1)")
manga.thumbnail_url = document.select("img[src^=http://img.bato.to/forums/uploads/]").first()?.attr("src")
manga.status = parseStatus(document.selectText("tr:contains(Status:) > td:eq(1)"))
manga.genre = tbody.select("tr:contains(Genres:) img").map { it.attr("alt") }.joinToString(", ")
}
private fun parseStatus(status: String?) = when (status) {
"Ongoing" -> Manga.ONGOING
"Complete" -> Manga.COMPLETED
else -> Manga.UNKNOWN
}
override fun chapterListParse(response: Response, chapters: MutableList<Chapter>) {
val body = response.body().string()
val matcher = staffNotice.matcher(body)
if (matcher.find()) {
val notice = Html.fromHtml(matcher.group(1)).toString().trim()
throw Exception(notice)
}
val document = Jsoup.parse(body)
for (element in document.select(chapterListSelector())) {
Chapter.create().apply {
chapterFromElement(element, this)
chapters.add(this)
}
}
}
override fun chapterListSelector() = "tr.row.lang_English.chapter_row"
override fun chapterFromElement(element: Element, chapter: Chapter) {
val urlElement = element.select("a[href^=http://bato.to/reader").first()
chapter.setUrl(urlElement.attr("href"))
chapter.name = urlElement.text()
chapter.date_upload = element.select("td").getOrNull(4)?.let {
parseDateFromElement(it)
} ?: 0
}
private fun parseDateFromElement(dateElement: Element): Long {
val dateAsString = dateElement.text()
val date: Date
try {
date = SimpleDateFormat("dd MMMMM yyyy - hh:mm a", Locale.ENGLISH).parse(dateAsString)
} catch (e: ParseException) {
val m = datePattern.matcher(dateAsString)
if (m.matches()) {
val number = m.group(1)
val amount = if (number.contains("A")) 1 else Integer.parseInt(m.group(1))
val unit = m.group(2)
date = Calendar.getInstance().apply {
add(dateFields[unit]!!, -amount)
}.time
} else {
return 0
}
}
return date.time
}
override fun pageListRequest(chapter: Chapter): Request {
val id = chapter.url.substringAfterLast("#")
return GET("$baseUrl/areader?id=$id&p=1", pageHeaders)
}
override fun pageListParse(document: Document, pages: MutableList<Page>) {
val selectElement = document.select("#page_select").first()
if (selectElement != null) {
for ((i, element) in selectElement.select("option").withIndex()) {
pages.add(Page(i, element.attr("value")))
}
pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
} else {
// For webtoons in one page
for ((i, element) in document.select("div > img").withIndex()) {
pages.add(Page(i, "", element.attr("src")))
}
}
}
override fun imageUrlRequest(page: Page): Request {
val pageUrl = page.url
val start = pageUrl.indexOf("#") + 1
val end = pageUrl.indexOf("_", start)
val id = pageUrl.substring(start, end)
return GET("$baseUrl/areader?id=$id&p=${pageUrl.substring(end+1)}", pageHeaders)
}
override fun imageUrlParse(document: Document): String {
return document.select("#comic_page").first().attr("src")
}
override fun login(username: String, password: String) =
client.newCall(GET("$baseUrl/forums/index.php?app=core&module=global&section=login", headers))
.asObservable()
.flatMap { doLogin(it.body().string(), username, password) }
.map { isAuthenticationSuccessful(it) }
private fun doLogin(response: String, username: String, password: String): Observable<Response> {
val doc = Jsoup.parse(response)
val form = doc.select("#login").first()
val url = form.attr("action")
val authKey = form.select("input[name=auth_key]").first()
val payload = FormBody.Builder().apply {
add(authKey.attr("name"), authKey.attr("value"))
add("ips_username", username)
add("ips_password", password)
add("invisible", "1")
add("rememberMe", "1")
}.build()
return client.newCall(POST(url, headers, payload)).asObservable()
}
override fun isAuthenticationSuccessful(response: Response) =
response.priorResponse() != null && response.priorResponse().code() == 302
override fun isLogged(): Boolean {
return network.cookies.get(URI(baseUrl)).any { it.name() == "pass_hash" }
}
override fun fetchChapterList(manga: Manga): Observable<List<Chapter>> {
if (!isLogged()) {
val username = preferences.sourceUsername(this)
val password = preferences.sourcePassword(this)
if (username.isNullOrEmpty() || password.isNullOrEmpty()) {
return Observable.error(Exception("User not logged"))
} else {
return login(username, password).flatMap { super.fetchChapterList(manga) }
}
} else {
return super.fetchChapterList(manga)
}
}
}

View File

@ -1,234 +0,0 @@
package eu.kanade.tachiyomi.data.source.online.english;
import android.content.Context;
import android.net.Uri;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.network.ReqKt;
import eu.kanade.tachiyomi.data.source.Language;
import eu.kanade.tachiyomi.data.source.LanguageKt;
import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.data.source.model.MangasPage;
import eu.kanade.tachiyomi.data.source.model.Page;
import eu.kanade.tachiyomi.util.Parser;
import okhttp3.FormBody;
import okhttp3.Headers;
import okhttp3.Request;
public class Kissmanga extends Source {
public static final String NAME = "Kissmanga";
public static final String HOST = "kissmanga.com";
public static final String IP = "93.174.95.110";
public static final String BASE_URL = "http://" + IP;
public static final String POPULAR_MANGAS_URL = BASE_URL + "/MangaList/MostPopular?page=%s";
public static final String SEARCH_URL = BASE_URL + "/AdvanceSearch";
public Kissmanga(Context context) {
super(context);
}
@Override
protected Headers.Builder headersBuilder() {
Headers.Builder builder = super.headersBuilder();
builder.add("Host", HOST);
return builder;
}
@Override
public String getName() {
return NAME;
}
@Override
public String getBaseUrl() {
return BASE_URL;
}
public Language getLang() {
return LanguageKt.getEN();
}
@Override
protected String getInitialPopularMangasUrl() {
return String.format(POPULAR_MANGAS_URL, 1);
}
@Override
protected String getInitialSearchUrl(String query) {
return SEARCH_URL;
}
@Override
protected Request searchMangaRequest(MangasPage page, String query) {
if (page.page == 1) {
page.url = getInitialSearchUrl(query);
}
FormBody.Builder form = new FormBody.Builder();
form.add("authorArtist", "");
form.add("mangaName", query);
form.add("status", "");
form.add("genres", "");
return ReqKt.post(page.url, getRequestHeaders(), form.build());
}
@Override
protected Request pageListRequest(String chapterUrl) {
return ReqKt.post(getBaseUrl() + chapterUrl, getRequestHeaders());
}
@Override
protected Request imageRequest(Page page) {
return ReqKt.get(page.getImageUrl());
}
@Override
protected List<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
List<Manga> mangaList = new ArrayList<>();
for (Element currentHtmlBlock : parsedHtml.select("table.listing tr:gt(1)")) {
Manga manga = constructPopularMangaFromHtml(currentHtmlBlock);
mangaList.add(manga);
}
return mangaList;
}
private Manga constructPopularMangaFromHtml(Element htmlBlock) {
Manga manga = new Manga();
manga.source = getId();
Element urlElement = Parser.element(htmlBlock, "td a:eq(0)");
if (urlElement != null) {
manga.setUrl(urlElement.attr("href"));
manga.title = urlElement.text();
}
return manga;
}
@Override
protected String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page) {
String path = Parser.href(parsedHtml, "li > a:contains( Next)");
return path != null ? BASE_URL + path : null;
}
@Override
protected List<Manga> parseSearchFromHtml(Document parsedHtml) {
return parsePopularMangasFromHtml(parsedHtml);
}
@Override
protected String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query) {
return null;
}
@Override
protected Manga parseHtmlToManga(String mangaUrl, String unparsedHtml) {
Document parsedDocument = Jsoup.parse(unparsedHtml);
Element infoElement = parsedDocument.select("div.barContent").first();
Manga manga = Manga.create(mangaUrl);
manga.title = Parser.text(infoElement, "a.bigChar");
manga.author = Parser.text(infoElement, "p:has(span:contains(Author:)) > a");
manga.genre = Parser.allText(infoElement, "p:has(span:contains(Genres:)) > *:gt(0)");
manga.description = Parser.allText(infoElement, "p:has(span:contains(Summary:)) ~ p");
manga.status = parseStatus(Parser.text(infoElement, "p:has(span:contains(Status:))"));
String thumbnail = Parser.src(parsedDocument, ".rightBox:eq(0) img");
if (thumbnail != null) {
manga.thumbnail_url = Uri.parse(thumbnail).buildUpon().authority(IP).toString();
}
manga.initialized = true;
return manga;
}
private int parseStatus(String status) {
if (status.contains("Ongoing")) {
return Manga.ONGOING;
}
if (status.contains("Completed")) {
return Manga.COMPLETED;
}
return Manga.UNKNOWN;
}
@Override
protected List<Chapter> parseHtmlToChapters(String unparsedHtml) {
Document parsedDocument = Jsoup.parse(unparsedHtml);
List<Chapter> chapterList = new ArrayList<>();
for (Element chapterElement : parsedDocument.select("table.listing tr:gt(1)")) {
Chapter chapter = constructChapterFromHtmlBlock(chapterElement);
chapterList.add(chapter);
}
return chapterList;
}
private Chapter constructChapterFromHtmlBlock(Element chapterElement) {
Chapter chapter = Chapter.create();
Element urlElement = Parser.element(chapterElement, "a");
String date = Parser.text(chapterElement, "td:eq(1)");
if (urlElement != null) {
chapter.setUrl(urlElement.attr("href"));
chapter.name = urlElement.text();
}
if (date != null) {
try {
chapter.date_upload = new SimpleDateFormat("MM/dd/yyyy", Locale.ENGLISH).parse(date).getTime();
} catch (ParseException e) { /* Ignore */ }
}
return chapter;
}
@Override
protected List<String> parseHtmlToPageUrls(String unparsedHtml) {
Document parsedDocument = Jsoup.parse(unparsedHtml);
List<String> pageUrlList = new ArrayList<>();
int numImages = parsedDocument.select("#divImage img").size();
for (int i = 0; i < numImages; i++) {
pageUrlList.add("");
}
return pageUrlList;
}
@Override
protected List<Page> parseFirstPage(List<? extends Page> pages, String unparsedHtml) {
Pattern p = Pattern.compile("lstImages.push\\(\"(.+?)\"");
Matcher m = p.matcher(unparsedHtml);
int i = 0;
while (m.find()) {
pages.get(i++).setImageUrl(m.group(1));
}
return (List<Page>) pages;
}
@Override
protected String parseHtmlToImageUrl(String unparsedHtml) {
return null;
}
}

View File

@ -0,0 +1,118 @@
package eu.kanade.tachiyomi.data.source.online.english
import android.content.Context
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.network.GET
import eu.kanade.tachiyomi.data.network.POST
import eu.kanade.tachiyomi.data.source.EN
import eu.kanade.tachiyomi.data.source.Language
import eu.kanade.tachiyomi.data.source.model.MangasPage
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
import okhttp3.FormBody
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.SimpleDateFormat
import java.util.regex.Pattern
class Kissmanga(context: Context, override val id: Int) : ParsedOnlineSource(context) {
override val name = "Kissmanga"
override val baseUrl = "http://kissmanga.com"
override val lang: Language get() = EN
override val client: OkHttpClient get() = network.cloudflareClient
override fun popularMangaInitialUrl() = "$baseUrl/MangaList/MostPopular"
override fun popularMangaSelector() = "table.listing tr:gt(1)"
override fun popularMangaFromElement(element: Element, manga: Manga) {
element.select("td a:eq(0)").first().let {
manga.setUrl(it.attr("href"))
manga.title = it.text()
}
}
override fun popularMangaNextPageSelector() = "li > a:contains( Next)"
override fun searchMangaRequest(page: MangasPage, query: String): Request {
if (page.page == 1) {
page.url = searchMangaInitialUrl(query)
}
val form = FormBody.Builder().apply {
add("authorArtist", "")
add("mangaName", query)
add("status", "")
add("genres", "")
}.build()
return POST(page.url, headers, form)
}
override fun searchMangaInitialUrl(query: String) = "$baseUrl/AdvanceSearch"
override fun searchMangaSelector() = popularMangaSelector()
override fun searchMangaFromElement(element: Element, manga: Manga) {
popularMangaFromElement(element, manga)
}
override fun searchMangaNextPageSelector() = null
override fun mangaDetailsParse(document: Document, manga: Manga) {
val infoElement = document.select("div.barContent").first()
manga.author = infoElement.select("p:has(span:contains(Author:)) > a").first()?.text()
manga.genre = infoElement.select("p:has(span:contains(Genres:)) > *:gt(0)").text()
manga.description = infoElement.select("p:has(span:contains(Summary:)) ~ p").text()
manga.status = infoElement.select("p:has(span:contains(Status:))").first()?.text().orEmpty().let { parseStatus(it)}
manga.thumbnail_url = document.select(".rightBox:eq(0) img").first()?.attr("src")
}
fun parseStatus(status: String) = when {
status.contains("Ongoing") -> Manga.ONGOING
status.contains("Completed") -> Manga.COMPLETED
else -> Manga.UNKNOWN
}
override fun chapterListSelector() = "table.listing tr:gt(1)"
override fun chapterFromElement(element: Element, chapter: Chapter) {
val urlElement = element.select("a").first()
chapter.setUrl(urlElement.attr("href"))
chapter.name = urlElement.text()
chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
SimpleDateFormat("MM/dd/yyyy").parse(it).time
} ?: 0
}
override fun pageListRequest(chapter: Chapter) = POST(baseUrl + chapter.url, headers)
override fun pageListParse(response: Response, pages: MutableList<Page>) {
//language=RegExp
val p = Pattern.compile("""lstImages.push\("(.+?)"""")
val m = p.matcher(response.body().string())
var i = 0
while (m.find()) {
pages.add(Page(i++, "", m.group(1)))
}
}
// Not used
override fun pageListParse(document: Document, pages: MutableList<Page>) {}
override fun imageUrlRequest(page: Page) = GET(page.url)
override fun imageUrlParse(document: Document) = ""
}

View File

@ -1,245 +0,0 @@
package eu.kanade.tachiyomi.data.source.online.english;
import android.content.Context;
import android.net.Uri;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.source.Language;
import eu.kanade.tachiyomi.data.source.LanguageKt;
import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.data.source.model.MangasPage;
import eu.kanade.tachiyomi.util.Parser;
public class Mangafox extends Source {
public static final String NAME = "Mangafox";
public static final String BASE_URL = "http://mangafox.me";
public static final String POPULAR_MANGAS_URL = BASE_URL + "/directory/%s";
public static final String SEARCH_URL =
BASE_URL + "/search.php?name_method=cw&advopts=1&order=za&sort=views&name=%s&page=%s";
public Mangafox(Context context) {
super(context);
}
@Override
public String getName() {
return NAME;
}
@Override
public String getBaseUrl() {
return BASE_URL;
}
public Language getLang() {
return LanguageKt.getEN();
}
@Override
protected String getInitialPopularMangasUrl() {
return String.format(POPULAR_MANGAS_URL, "");
}
@Override
protected String getInitialSearchUrl(String query) {
return String.format(SEARCH_URL, Uri.encode(query), 1);
}
@Override
protected List<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
List<Manga> mangaList = new ArrayList<>();
for (Element currentHtmlBlock : parsedHtml.select("div#mangalist > ul.list > li")) {
Manga currentManga = constructPopularMangaFromHtmlBlock(currentHtmlBlock);
mangaList.add(currentManga);
}
return mangaList;
}
private Manga constructPopularMangaFromHtmlBlock(Element htmlBlock) {
Manga manga = new Manga();
manga.source = getId();
Element urlElement = Parser.element(htmlBlock, "a.title");
if (urlElement != null) {
manga.setUrl(urlElement.attr("href"));
manga.title = urlElement.text();
}
return manga;
}
@Override
protected String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page) {
Element next = Parser.element(parsedHtml, "a:has(span.next)");
return next != null ? String.format(POPULAR_MANGAS_URL, next.attr("href")) : null;
}
@Override
protected List<Manga> parseSearchFromHtml(Document parsedHtml) {
List<Manga> mangaList = new ArrayList<>();
for (Element currentHtmlBlock : parsedHtml.select("table#listing > tbody > tr:gt(0)")) {
Manga currentManga = constructSearchMangaFromHtmlBlock(currentHtmlBlock);
mangaList.add(currentManga);
}
return mangaList;
}
private Manga constructSearchMangaFromHtmlBlock(Element htmlBlock) {
Manga mangaFromHtmlBlock = new Manga();
mangaFromHtmlBlock.source = getId();
Element urlElement = Parser.element(htmlBlock, "a.series_preview");
if (urlElement != null) {
mangaFromHtmlBlock.setUrl(urlElement.attr("href"));
mangaFromHtmlBlock.title = urlElement.text();
}
return mangaFromHtmlBlock;
}
@Override
protected String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query) {
Element next = Parser.element(parsedHtml, "a:has(span.next)");
return next != null ? BASE_URL + next.attr("href") : null;
}
@Override
protected Manga parseHtmlToManga(String mangaUrl, String unparsedHtml) {
Document parsedDocument = Jsoup.parse(unparsedHtml);
Element infoElement = parsedDocument.select("div#title").first();
Element rowElement = infoElement.select("table > tbody > tr:eq(1)").first();
Element sideInfoElement = parsedDocument.select("#series_info").first();
Manga manga = Manga.create(mangaUrl);
manga.author = Parser.text(rowElement, "td:eq(1)");
manga.artist = Parser.text(rowElement, "td:eq(2)");
manga.description = Parser.text(infoElement, "p.summary");
manga.genre = Parser.text(rowElement, "td:eq(3)");
manga.thumbnail_url = Parser.src(sideInfoElement, "div.cover > img");
manga.status = parseStatus(Parser.text(sideInfoElement, ".data"));
manga.initialized = true;
return manga;
}
private int parseStatus(String status) {
if (status.contains("Ongoing")) {
return Manga.ONGOING;
}
if (status.contains("Completed")) {
return Manga.COMPLETED;
}
return Manga.UNKNOWN;
}
@Override
protected List<Chapter> parseHtmlToChapters(String unparsedHtml) {
Document parsedDocument = Jsoup.parse(unparsedHtml);
List<Chapter> chapterList = new ArrayList<>();
for (Element chapterElement : parsedDocument.select("div#chapters li div")) {
Chapter currentChapter = constructChapterFromHtmlBlock(chapterElement);
chapterList.add(currentChapter);
}
return chapterList;
}
private Chapter constructChapterFromHtmlBlock(Element chapterElement) {
Chapter chapter = Chapter.create();
Element urlElement = chapterElement.select("a.tips").first();
Element dateElement = chapterElement.select("span.date").first();
if (urlElement != null) {
chapter.setUrl(urlElement.attr("href"));
chapter.name = urlElement.text();
}
if (dateElement != null) {
chapter.date_upload = parseUpdateFromElement(dateElement);
}
return chapter;
}
private long parseUpdateFromElement(Element updateElement) {
String updatedDateAsString = updateElement.text();
if (updatedDateAsString.contains("Today")) {
Calendar today = Calendar.getInstance();
today.set(Calendar.HOUR_OF_DAY, 0);
today.set(Calendar.MINUTE, 0);
today.set(Calendar.SECOND, 0);
today.set(Calendar.MILLISECOND, 0);
try {
Date withoutDay = new SimpleDateFormat("h:mm a", Locale.ENGLISH).parse(updatedDateAsString.replace("Today", ""));
return today.getTimeInMillis() + withoutDay.getTime();
} catch (ParseException e) {
return today.getTimeInMillis();
}
} else if (updatedDateAsString.contains("Yesterday")) {
Calendar yesterday = Calendar.getInstance();
yesterday.add(Calendar.DATE, -1);
yesterday.set(Calendar.HOUR_OF_DAY, 0);
yesterday.set(Calendar.MINUTE, 0);
yesterday.set(Calendar.SECOND, 0);
yesterday.set(Calendar.MILLISECOND, 0);
try {
Date withoutDay = new SimpleDateFormat("h:mm a", Locale.ENGLISH).parse(updatedDateAsString.replace("Yesterday", ""));
return yesterday.getTimeInMillis() + withoutDay.getTime();
} catch (ParseException e) {
return yesterday.getTimeInMillis();
}
} else {
try {
Date specificDate = new SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(updatedDateAsString);
return specificDate.getTime();
} catch (ParseException e) {
// Do Nothing.
}
}
return 0;
}
@Override
protected List<String> parseHtmlToPageUrls(String unparsedHtml) {
Document parsedDocument = Jsoup.parse(unparsedHtml);
List<String> pageUrlList = new ArrayList<>();
Elements pageUrlElements = parsedDocument.select("select.m").first().select("option:not([value=0])");
String baseUrl = parsedDocument.select("div#series a").first().attr("href").replace("1.html", "");
for (Element pageUrlElement : pageUrlElements) {
pageUrlList.add(baseUrl + pageUrlElement.attr("value") + ".html");
}
return pageUrlList;
}
@Override
protected String parseHtmlToImageUrl(String unparsedHtml) {
Document parsedDocument = Jsoup.parse(unparsedHtml);
Element imageElement = parsedDocument.getElementById("image");
return imageElement.attr("src");
}
}

View File

@ -0,0 +1,121 @@
package eu.kanade.tachiyomi.data.source.online.english
import android.content.Context
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.source.EN
import eu.kanade.tachiyomi.data.source.Language
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
class Mangafox(context: Context, override val id: Int) : ParsedOnlineSource(context) {
override val name = "Mangafox"
override val baseUrl = "http://mangafox.me"
override val lang: Language get() = EN
override fun popularMangaInitialUrl() = "$baseUrl/directory/"
override fun popularMangaSelector() = "div#mangalist > ul.list > li"
override fun popularMangaFromElement(element: Element, manga: Manga) {
element.select("a.title").first().let {
manga.setUrl(it.attr("href"))
manga.title = it.text()
}
}
override fun popularMangaNextPageSelector() = "a:has(span.next)"
override fun searchMangaInitialUrl(query: String) =
"$baseUrl/search.php?name_method=cw&advopts=1&order=za&sort=views&name=$query&page=1"
override fun searchMangaSelector() = "table#listing > tbody > tr:gt(0)"
override fun searchMangaFromElement(element: Element, manga: Manga) {
element.select("a.series_preview").first().let {
manga.setUrl(it.attr("href"))
manga.title = it.text()
}
}
override fun searchMangaNextPageSelector() = "a:has(span.next)"
override fun mangaDetailsParse(document: Document, manga: Manga) {
val infoElement = document.select("div#title").first()
val rowElement = infoElement.select("table > tbody > tr:eq(1)").first()
val sideInfoElement = document.select("#series_info").first()
manga.author = rowElement.select("td:eq(1)").first()?.text()
manga.artist = rowElement.select("td:eq(2)").first()?.text()
manga.genre = rowElement.select("td:eq(3)").first()?.text()
manga.description = infoElement.select("p.summary").first()?.text()
manga.status = sideInfoElement.select(".data").first()?.text().orEmpty().let { parseStatus(it) }
manga.thumbnail_url = sideInfoElement.select("div.cover > img").first()?.attr("src")
}
private fun parseStatus(status: String) = when {
status.contains("Ongoing") -> Manga.ONGOING
status.contains("Completed") -> Manga.COMPLETED
else -> Manga.UNKNOWN
}
override fun chapterListSelector() = "div#chapters li div"
override fun chapterFromElement(element: Element, chapter: Chapter) {
val urlElement = element.select("a.tips").first()
chapter.setUrl(urlElement.attr("href"))
chapter.name = urlElement.text()
chapter.date_upload = element.select("span.date").first()?.text()?.let { parseChapterDate(it) } ?: 0
}
private fun parseChapterDate(date: String): Long {
return if ("Today" in date || " ago" in date) {
Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
} else if ("Yesterday" in date) {
Calendar.getInstance().apply {
add(Calendar.DATE, -1)
set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
} else {
try {
SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(date).time
} catch (e: ParseException) {
0L
}
}
}
override fun pageListParse(response: Response, pages: MutableList<Page>) {
val document = Jsoup.parse(response.body().string())
val url = response.request().url().toString().substringBeforeLast('/')
document.select("select.m").first().select("option:not([value=0])").forEach {
pages.add(Page(pages.size, "$url/${it.attr("value")}.html"))
}
}
// Not used, overrides parent.
override fun pageListParse(document: Document, pages: MutableList<Page>) {}
override fun imageUrlParse(document: Document) = document.getElementById("image").attr("src")
}

View File

@ -1,313 +0,0 @@
package eu.kanade.tachiyomi.data.source.online.english;
import android.content.Context;
import android.net.Uri;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.source.Language;
import eu.kanade.tachiyomi.data.source.LanguageKt;
import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.data.source.model.MangasPage;
import eu.kanade.tachiyomi.util.Parser;
public class Mangahere extends Source {
public static final String NAME = "Mangahere";
public static final String BASE_URL = "http://www.mangahere.co";
public static final String POPULAR_MANGAS_URL = BASE_URL + "/directory/%s";
public static final String SEARCH_URL = BASE_URL + "/search.php?name=%s&page=%s&sort=views&order=za";
public Mangahere(Context context) {
super(context);
}
@Override
public String getName() {
return NAME;
}
@Override
public String getBaseUrl() {
return BASE_URL;
}
public Language getLang() {
return LanguageKt.getEN();
}
@Override
protected String getInitialPopularMangasUrl() {
return String.format(POPULAR_MANGAS_URL, "");
}
@Override
protected String getInitialSearchUrl(String query) {
return String.format(SEARCH_URL, Uri.encode(query), 1);
}
@Override
public List<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
List<Manga> mangaList = new ArrayList<>();
for (Element currentHtmlBlock : parsedHtml.select("div.directory_list > ul > li")) {
Manga currentManga = constructPopularMangaFromHtmlBlock(currentHtmlBlock);
mangaList.add(currentManga);
}
return mangaList;
}
private Manga constructPopularMangaFromHtmlBlock(Element htmlBlock) {
Manga manga = new Manga();
manga.source = getId();
Element urlElement = Parser.element(htmlBlock, "div.title > a");
if (urlElement != null) {
manga.setUrl(urlElement.attr("href"));
manga.title = urlElement.attr("title");
}
return manga;
}
@Override
protected String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page) {
Element next = Parser.element(parsedHtml, "div.next-page > a.next");
return next != null ? String.format(POPULAR_MANGAS_URL, next.attr("href")) : null;
}
@Override
protected List<Manga> parseSearchFromHtml(Document parsedHtml) {
List<Manga> mangaList = new ArrayList<>();
Elements mangaHtmlBlocks = parsedHtml.select("div.result_search > dl");
for (Element currentHtmlBlock : mangaHtmlBlocks) {
Manga currentManga = constructSearchMangaFromHtmlBlock(currentHtmlBlock);
mangaList.add(currentManga);
}
return mangaList;
}
private Manga constructSearchMangaFromHtmlBlock(Element htmlBlock) {
Manga manga = new Manga();
manga.source = getId();
Element urlElement = Parser.element(htmlBlock, "a.manga_info");
if (urlElement != null) {
manga.setUrl(urlElement.attr("href"));
manga.title = urlElement.text();
}
return manga;
}
@Override
protected String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query) {
Element next = Parser.element(parsedHtml, "div.next-page > a.next");
return next != null ? BASE_URL + next.attr("href") : null;
}
private long parseUpdateFromElement(Element updateElement) {
String updatedDateAsString = updateElement.text();
if (updatedDateAsString.contains("Today")) {
Calendar today = Calendar.getInstance();
today.set(Calendar.HOUR_OF_DAY, 0);
today.set(Calendar.MINUTE, 0);
today.set(Calendar.SECOND, 0);
today.set(Calendar.MILLISECOND, 0);
try {
Date withoutDay = new SimpleDateFormat("MMM d, yyyy h:mma", Locale.ENGLISH).parse(updatedDateAsString.replace("Today", ""));
return today.getTimeInMillis() + withoutDay.getTime();
} catch (ParseException e) {
return today.getTimeInMillis();
}
} else if (updatedDateAsString.contains("Yesterday")) {
Calendar yesterday = Calendar.getInstance();
yesterday.add(Calendar.DATE, -1);
yesterday.set(Calendar.HOUR_OF_DAY, 0);
yesterday.set(Calendar.MINUTE, 0);
yesterday.set(Calendar.SECOND, 0);
yesterday.set(Calendar.MILLISECOND, 0);
try {
Date withoutDay = new SimpleDateFormat("MMM d, yyyy h:mma", Locale.ENGLISH).parse(updatedDateAsString.replace("Yesterday", ""));
return yesterday.getTimeInMillis() + withoutDay.getTime();
} catch (ParseException e) {
return yesterday.getTimeInMillis();
}
} else {
try {
Date specificDate = new SimpleDateFormat("MMM d, yyyy h:mma", Locale.ENGLISH).parse(updatedDateAsString);
return specificDate.getTime();
} catch (ParseException e) {
// Do Nothing.
}
}
return 0;
}
public Manga parseHtmlToManga(String mangaUrl, String unparsedHtml) {
int beginIndex = unparsedHtml.indexOf("<ul class=\"detail_topText\">");
int endIndex = unparsedHtml.indexOf("</ul>", beginIndex);
String trimmedHtml = unparsedHtml.substring(beginIndex, endIndex);
Document parsedDocument = Jsoup.parse(trimmedHtml);
Element detailElement = parsedDocument.select("ul.detail_topText").first();
Manga manga = Manga.create(mangaUrl);
manga.author = Parser.text(parsedDocument, "a[href^=http://www.mangahere.co/author/]");
manga.artist = Parser.text(parsedDocument, "a[href^=http://www.mangahere.co/artist/]");
String description = Parser.text(detailElement, "#show");
if (description != null) {
manga.description = description.substring(0, description.length() - "Show less".length());
}
String genres = Parser.text(detailElement, "li:eq(3)");
if (genres != null) {
manga.genre = genres.substring("Genre(s):".length());
}
manga.status = parseStatus(Parser.text(detailElement, "li:eq(6)"));
beginIndex = unparsedHtml.indexOf("<img");
endIndex = unparsedHtml.indexOf("/>", beginIndex);
trimmedHtml = unparsedHtml.substring(beginIndex, endIndex + 2);
parsedDocument = Jsoup.parse(trimmedHtml);
manga.thumbnail_url = Parser.src(parsedDocument, "img");
manga.initialized = true;
return manga;
}
private int parseStatus(String status) {
if (status.contains("Ongoing")) {
return Manga.ONGOING;
}
if (status.contains("Completed")) {
return Manga.COMPLETED;
}
return Manga.UNKNOWN;
}
@Override
public List<Chapter> parseHtmlToChapters(String unparsedHtml) {
int beginIndex = unparsedHtml.indexOf("<ul>");
int endIndex = unparsedHtml.indexOf("</ul>", beginIndex);
String trimmedHtml = unparsedHtml.substring(beginIndex, endIndex);
Document parsedDocument = Jsoup.parse(trimmedHtml);
List<Chapter> chapterList = new ArrayList<>();
for (Element chapterElement : parsedDocument.getElementsByTag("li")) {
Chapter currentChapter = constructChapterFromHtmlBlock(chapterElement);
chapterList.add(currentChapter);
}
return chapterList;
}
private Chapter constructChapterFromHtmlBlock(Element chapterElement) {
Chapter chapter = Chapter.create();
Element urlElement = chapterElement.select("a").first();
Element dateElement = chapterElement.select("span.right").first();
if (urlElement != null) {
chapter.setUrl(urlElement.attr("href"));
chapter.name = urlElement.text();
}
if (dateElement != null) {
chapter.date_upload = parseDateFromElement(dateElement);
}
return chapter;
}
private long parseDateFromElement(Element dateElement) {
String dateAsString = dateElement.text();
if (dateAsString.contains("Today")) {
Calendar today = Calendar.getInstance();
today.set(Calendar.HOUR_OF_DAY, 0);
today.set(Calendar.MINUTE, 0);
today.set(Calendar.SECOND, 0);
today.set(Calendar.MILLISECOND, 0);
try {
Date withoutDay = new SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(dateAsString.replace("Today", ""));
return today.getTimeInMillis() + withoutDay.getTime();
} catch (ParseException e) {
return today.getTimeInMillis();
}
} else if (dateAsString.contains("Yesterday")) {
Calendar yesterday = Calendar.getInstance();
yesterday.add(Calendar.DATE, -1);
yesterday.set(Calendar.HOUR_OF_DAY, 0);
yesterday.set(Calendar.MINUTE, 0);
yesterday.set(Calendar.SECOND, 0);
yesterday.set(Calendar.MILLISECOND, 0);
try {
Date withoutDay = new SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(dateAsString.replace("Yesterday", ""));
return yesterday.getTimeInMillis() + withoutDay.getTime();
} catch (ParseException e) {
return yesterday.getTimeInMillis();
}
} else {
try {
Date date = new SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(dateAsString);
return date.getTime();
} catch (ParseException e) {
// Do Nothing.
}
}
return 0;
}
@Override
public List<String> parseHtmlToPageUrls(String unparsedHtml) {
int beginIndex = unparsedHtml.indexOf("<div class=\"go_page clearfix\">");
int endIndex = unparsedHtml.indexOf("</div>", beginIndex);
String trimmedHtml = unparsedHtml.substring(beginIndex, endIndex);
Document parsedDocument = Jsoup.parse(trimmedHtml);
List<String> pageUrlList = new ArrayList<>();
Elements pageUrlElements = parsedDocument.select("select.wid60").first().getElementsByTag("option");
for (Element pageUrlElement : pageUrlElements) {
pageUrlList.add(pageUrlElement.attr("value"));
}
return pageUrlList;
}
@Override
public String parseHtmlToImageUrl(String unparsedHtml) {
int beginIndex = unparsedHtml.indexOf("<section class=\"read_img\" id=\"viewer\">");
int endIndex = unparsedHtml.indexOf("</section>", beginIndex);
String trimmedHtml = unparsedHtml.substring(beginIndex, endIndex);
Document parsedDocument = Jsoup.parse(trimmedHtml);
Element imageElement = parsedDocument.getElementById("image");
return imageElement.attr("src");
}
}

View File

@ -0,0 +1,113 @@
package eu.kanade.tachiyomi.data.source.online.english
import android.content.Context
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.source.EN
import eu.kanade.tachiyomi.data.source.Language
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
class Mangahere(context: Context, override val id: Int) : ParsedOnlineSource(context) {
override val name = "Mangahere"
override val baseUrl = "http://www.mangahere.co"
override val lang: Language get() = EN
override fun popularMangaInitialUrl() = "$baseUrl/directory/"
override fun popularMangaSelector() = "div.directory_list > ul > li"
override fun popularMangaFromElement(element: Element, manga: Manga) {
element.select("div.title > a").first().let {
manga.setUrl(it.attr("href"))
manga.title = it.text()
}
}
override fun popularMangaNextPageSelector() = "div.next-page > a.next"
override fun searchMangaInitialUrl(query: String) =
"$baseUrl/search.php?name=$query&page=1&sort=views&order=za"
override fun searchMangaSelector() = "div.result_search > dl:has(dt)"
override fun searchMangaFromElement(element: Element, manga: Manga) {
element.select("a.manga_info").first().let {
manga.setUrl(it.attr("href"))
manga.title = it.text()
}
}
override fun searchMangaNextPageSelector() = "div.next-page > a.next"
override fun mangaDetailsParse(document: Document, manga: Manga) {
val detailElement = document.select(".manga_detail_top").first()
val infoElement = detailElement.select(".detail_topText").first()
manga.author = infoElement.select("a[href^=http://www.mangahere.co/author/]").first()?.text()
manga.artist = infoElement.select("a[href^=http://www.mangahere.co/artist/]").first()?.text()
manga.genre = infoElement.select("li:eq(3)").first()?.text()?.substringAfter("Genre(s):")
manga.description = infoElement.select("#show").first()?.text()?.substringBeforeLast("Show less")
manga.status = infoElement.select("li:eq(6)").first()?.text().orEmpty().let { parseStatus(it) }
manga.thumbnail_url = detailElement.select("img.img").first()?.attr("src")
}
private fun parseStatus(status: String) = when {
status.contains("Ongoing") -> Manga.ONGOING
status.contains("Completed") -> Manga.COMPLETED
else -> Manga.UNKNOWN
}
override fun chapterListSelector() = ".detail_list > ul:not([class]) > li"
override fun chapterFromElement(element: Element, chapter: Chapter) {
val urlElement = element.select("a").first()
chapter.setUrl(urlElement.attr("href"))
chapter.name = urlElement.text()
chapter.date_upload = element.select("span.right").first()?.text()?.let { parseChapterDate(it) } ?: 0
}
private fun parseChapterDate(date: String): Long {
return if ("Today" in date) {
Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
} else if ("Yesterday" in date) {
Calendar.getInstance().apply {
add(Calendar.DATE, -1)
set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
} else {
try {
SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(date).time
} catch (e: ParseException) {
0L
}
}
}
override fun pageListParse(document: Document, pages: MutableList<Page>) {
document.select("select.wid60").first().getElementsByTag("option").forEach {
pages.add(Page(pages.size, it.attr("value")))
}
pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
}
override fun imageUrlParse(document: Document) = document.getElementById("image").attr("src")
}

View File

@ -1,290 +0,0 @@
package eu.kanade.tachiyomi.data.source.online.english;
import android.content.Context;
import android.net.Uri;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.source.Language;
import eu.kanade.tachiyomi.data.source.LanguageKt;
import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.data.source.model.MangasPage;
import eu.kanade.tachiyomi.util.Parser;
import okhttp3.Headers;
import rx.Observable;
import rx.functions.Action1;
import rx.functions.Func1;
public class ReadMangaToday extends Source {
public static final String NAME = "ReadMangaToday";
public static final String BASE_URL = "http://www.readmanga.today";
public static final String POPULAR_MANGAS_URL = BASE_URL + "/hot-manga/%s";
public static final String SEARCH_URL = BASE_URL + "/service/search?q=%s";
private static JsonParser parser = new JsonParser();
private static Gson gson = new Gson();
public ReadMangaToday(Context context) {
super(context);
}
@Override
public String getName() {
return NAME;
}
@Override
public String getBaseUrl() {
return BASE_URL;
}
@Override
protected String getInitialPopularMangasUrl() {
return String.format(POPULAR_MANGAS_URL, "");
}
@Override
protected String getInitialSearchUrl(String query) {
return String.format(SEARCH_URL, Uri.encode(query), 1);
}
@Override
public Language getLang() {
return LanguageKt.getEN();
}
@Override
public List<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
List<Manga> mangaList = new ArrayList<>();
for (Element currentHtmlBlock : parsedHtml.select("div.hot-manga > div.style-list > div.box")) {
Manga currentManga = constructPopularMangaFromHtmlBlock(currentHtmlBlock);
mangaList.add(currentManga);
}
return mangaList;
}
private Manga constructPopularMangaFromHtmlBlock(Element htmlBlock) {
Manga manga = new Manga();
manga.source = getId();
Element urlElement = Parser.element(htmlBlock, "div.title > h2 > a");
if (urlElement != null) {
manga.setUrl(urlElement.attr("href"));
manga.title = urlElement.attr("title");
}
return manga;
}
@Override
protected String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page) {
Element next = Parser.element(parsedHtml, "div.hot-manga > ul.pagination > li > a:contains(»)");
return next != null ? next.attr("href") : null;
}
@Override
public Observable<MangasPage> searchMangasFromNetwork(final MangasPage page, String query) {
return networkService
.requestBody(searchMangaRequest(page, query), true)
.doOnNext(new Action1<String>() {
@Override
public void call(String doc) {
page.mangas = ReadMangaToday.this.parseSearchFromJson(doc);
}
})
.map(new Func1<String, MangasPage>() {
@Override
public MangasPage call(String response) {
return page;
}
});
}
@Override
protected Headers.Builder headersBuilder() {
return super.headersBuilder().add("X-Requested-With", "XMLHttpRequest");
}
protected List<Manga> parseSearchFromJson(String unparsedJson) {
List<Manga> mangaList = new ArrayList<>();
JsonArray mangasArray = parser.parse(unparsedJson).getAsJsonArray();
for (JsonElement mangaElement : mangasArray) {
Manga currentManga = constructSearchMangaFromJsonObject(mangaElement.getAsJsonObject());
mangaList.add(currentManga);
}
return mangaList;
}
private Manga constructSearchMangaFromJsonObject(JsonObject jsonObject) {
Manga manga = new Manga();
manga.source = getId();
manga.setUrl(gson.fromJson(jsonObject.get("url"), String.class));
manga.title = gson.fromJson(jsonObject.get("title"), String.class);
return manga;
}
@Override
protected List<Manga> parseSearchFromHtml(Document parsedHtml) {
return null;
}
@Override
protected String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query) {
return null;
}
public Manga parseHtmlToManga(String mangaUrl, String unparsedHtml) {
int beginIndex = unparsedHtml.indexOf("<!-- content start -->");
int endIndex = unparsedHtml.indexOf("<!-- /content-end -->", beginIndex);
String trimmedHtml = unparsedHtml.substring(beginIndex, endIndex);
Document parsedDocument = Jsoup.parse(trimmedHtml);
Element detailElement = parsedDocument.select("div.movie-meta").first();
Manga manga = Manga.create(mangaUrl);
for (Element castHtmlBlock : parsedDocument.select("div.cast ul.cast-list > li")) {
String name = Parser.text(castHtmlBlock, "ul > li > a");
String role = Parser.text(castHtmlBlock, "ul > li:eq(1)");
if (role.equals("Author")) {
manga.author = name;
} else if (role.equals("Artist")) {
manga.artist = name;
}
}
String description = Parser.text(detailElement, "li.movie-detail");
if (description != null) {
manga.description = description;
}
String genres = Parser.text(detailElement, "dl.dl-horizontal > dd:eq(5)");
if (genres != null) {
manga.genre = genres;
}
manga.status = parseStatus(Parser.text(detailElement, "dl.dl-horizontal > dd:eq(3)"));
manga.thumbnail_url = Parser.src(detailElement, "img.img-responsive");
manga.initialized = true;
return manga;
}
private int parseStatus(String status) {
if (status.contains("Ongoing")) {
return Manga.ONGOING;
} else if (status.contains("Completed")) {
return Manga.COMPLETED;
}
return Manga.UNKNOWN;
}
@Override
public List<Chapter> parseHtmlToChapters(String unparsedHtml) {
int beginIndex = unparsedHtml.indexOf("<!-- content start -->");
int endIndex = unparsedHtml.indexOf("<!-- /content-end -->", beginIndex);
String trimmedHtml = unparsedHtml.substring(beginIndex, endIndex);
Document parsedDocument = Jsoup.parse(trimmedHtml);
List<Chapter> chapterList = new ArrayList<>();
for (Element chapterElement : parsedDocument.select("ul.chp_lst > li")) {
Chapter currentChapter = constructChapterFromHtmlBlock(chapterElement);
chapterList.add(currentChapter);
}
return chapterList;
}
private Chapter constructChapterFromHtmlBlock(Element chapterElement) {
Chapter chapter = Chapter.create();
Element urlElement = chapterElement.select("a").first();
Element dateElement = chapterElement.select("span.dte").first();
if (urlElement != null) {
chapter.setUrl(urlElement.attr("href"));
chapter.name = urlElement.select("span.val").text();
}
if (dateElement != null) {
chapter.date_upload = parseDateFromElement(dateElement);
}
return chapter;
}
private long parseDateFromElement(Element dateElement) {
String dateAsString = dateElement.text();
String[] dateWords = dateAsString.split(" ");
if (dateWords.length == 3) {
int timeAgo = Integer.parseInt(dateWords[0]);
Calendar date = Calendar.getInstance();
if (dateWords[1].contains("Minute")) {
date.add(Calendar.MINUTE, - timeAgo);
} else if (dateWords[1].contains("Hour")) {
date.add(Calendar.HOUR_OF_DAY, - timeAgo);
} else if (dateWords[1].contains("Day")) {
date.add(Calendar.DAY_OF_YEAR, -timeAgo);
} else if (dateWords[1].contains("Week")) {
date.add(Calendar.WEEK_OF_YEAR, -timeAgo);
} else if (dateWords[1].contains("Month")) {
date.add(Calendar.MONTH, -timeAgo);
} else if (dateWords[1].contains("Year")) {
date.add(Calendar.YEAR, -timeAgo);
}
return date.getTimeInMillis();
}
return 0;
}
@Override
public List<String> parseHtmlToPageUrls(String unparsedHtml) {
int beginIndex = unparsedHtml.indexOf("<!-- content start -->");
int endIndex = unparsedHtml.indexOf("<!-- /content-end -->", beginIndex);
String trimmedHtml = unparsedHtml.substring(beginIndex, endIndex);
Document parsedDocument = Jsoup.parse(trimmedHtml);
List<String> pageUrlList = new ArrayList<>();
Elements pageUrlElements = parsedDocument.select("ul.list-switcher-2 > li > select.jump-menu").first().getElementsByTag("option");
for (Element pageUrlElement : pageUrlElements) {
pageUrlList.add(pageUrlElement.attr("value"));
}
return pageUrlList;
}
@Override
public String parseHtmlToImageUrl(String unparsedHtml) {
int beginIndex = unparsedHtml.indexOf("<!-- content start -->");
int endIndex = unparsedHtml.indexOf("<!-- /content-end -->", beginIndex);
String trimmedHtml = unparsedHtml.substring(beginIndex, endIndex);
Document parsedDocument = Jsoup.parse(trimmedHtml);
Element imageElement = Parser.element(parsedDocument, "img.img-responsive-2");
return imageElement.attr("src");
}
}

View File

@ -0,0 +1,127 @@
package eu.kanade.tachiyomi.data.source.online.english
import android.content.Context
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.network.POST
import eu.kanade.tachiyomi.data.source.EN
import eu.kanade.tachiyomi.data.source.Language
import eu.kanade.tachiyomi.data.source.model.MangasPage
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
import okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.util.*
class Readmangatoday(context: Context, override val id: Int) : ParsedOnlineSource(context) {
override val name = "ReadMangaToday"
override val baseUrl = "http://www.readmanga.today"
override val lang: Language get() = EN
override fun popularMangaInitialUrl() = "$baseUrl/hot-manga/"
override fun popularMangaSelector() = "div.hot-manga > div.style-list > div.box"
override fun popularMangaFromElement(element: Element, manga: Manga) {
element.select("div.title > h2 > a").first().let {
manga.setUrl(it.attr("href"))
manga.title = it.attr("title")
}
}
override fun popularMangaNextPageSelector() = "div.hot-manga > ul.pagination > li > a:contains(»)"
override fun searchMangaInitialUrl(query: String) =
"$baseUrl/search"
override fun searchMangaRequest(page: MangasPage, query: String): Request {
if (page.page == 1) {
page.url = searchMangaInitialUrl(query)
}
var builder = okhttp3.FormBody.Builder()
builder.add("query", query)
return POST(page.url, headers, builder.build())
}
override fun searchMangaSelector() = "div.content-list > div.style-list > div.box"
override fun searchMangaFromElement(element: Element, manga: Manga) {
element.select("div.title > h2 > a").first().let {
manga.setUrl(it.attr("href"))
manga.title = it.attr("title")
}
}
override fun searchMangaNextPageSelector() = "div.next-page > a.next"
override fun mangaDetailsParse(document: Document, manga: Manga) {
val detailElement = document.select("div.movie-meta").first()
manga.author = document.select("ul.cast-list li.director > ul a").first()?.text()
manga.artist = document.select("ul.cast-list li:not(.director) > ul a").first()?.text()
manga.genre = detailElement.select("dl.dl-horizontal > dd:eq(5)").first()?.text()
manga.description = detailElement.select("li.movie-detail").first()?.text()
manga.status = detailElement.select("dl.dl-horizontal > dd:eq(3)").first()?.text().orEmpty().let { parseStatus(it) }
manga.thumbnail_url = detailElement.select("img.img-responsive").first()?.attr("src")
}
private fun parseStatus(status: String) = when {
status.contains("Ongoing") -> Manga.ONGOING
status.contains("Completed") -> Manga.COMPLETED
else -> Manga.UNKNOWN
}
override fun chapterListSelector() = "ul.chp_lst > li"
override fun chapterFromElement(element: Element, chapter: Chapter) {
val urlElement = element.select("a").first()
chapter.setUrl(urlElement.attr("href"))
chapter.name = urlElement.select("span.val").text()
chapter.date_upload = element.select("span.dte").first()?.text()?.let { parseChapterDate(it) } ?: 0
}
private fun parseChapterDate(date: String): Long {
val dateWords : List<String> = date.split(" ")
if (dateWords.size == 3) {
val timeAgo = Integer.parseInt(dateWords[0])
var date : Calendar = Calendar.getInstance()
if (dateWords[1].contains("Minute")) {
date.add(Calendar.MINUTE, - timeAgo)
} else if (dateWords[1].contains("Hour")) {
date.add(Calendar.HOUR_OF_DAY, - timeAgo)
} else if (dateWords[1].contains("Day")) {
date.add(Calendar.DAY_OF_YEAR, -timeAgo)
} else if (dateWords[1].contains("Week")) {
date.add(Calendar.WEEK_OF_YEAR, -timeAgo)
} else if (dateWords[1].contains("Month")) {
date.add(Calendar.MONTH, -timeAgo)
} else if (dateWords[1].contains("Year")) {
date.add(Calendar.YEAR, -timeAgo)
}
return date.getTimeInMillis()
}
return 0L
}
override fun pageListParse(document: Document, pages: MutableList<Page>) {
document.select("ul.list-switcher-2 > li > select.jump-menu").first().getElementsByTag("option").forEach {
pages.add(Page(pages.size, it.attr("value")))
}
pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
}
override fun imageUrlParse(document: Document) = document.select("img.img-responsive-2").first().attr("src")
}

View File

@ -1,240 +0,0 @@
package eu.kanade.tachiyomi.data.source.online.russian;
import android.content.Context;
import android.net.Uri;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.source.Language;
import eu.kanade.tachiyomi.data.source.LanguageKt;
import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.data.source.model.MangasPage;
import eu.kanade.tachiyomi.data.source.model.Page;
import eu.kanade.tachiyomi.util.Parser;
public class Mangachan extends Source {
public static final String NAME = "Mangachan";
public static final String BASE_URL = "http://mangachan.ru";
public static final String POPULAR_MANGAS_URL = BASE_URL + "/mostfavorites";
public static final String SEARCH_URL = BASE_URL + "/?do=search&subaction=search&story=%s";
public Mangachan(Context context) {
super(context);
}
@Override
public Language getLang() {
return LanguageKt.getRU();
}
@Override
public String getName() {
return NAME;
}
@Override
public String getBaseUrl() {
return BASE_URL;
}
@Override
protected String getInitialPopularMangasUrl() {
return POPULAR_MANGAS_URL;
}
@Override
protected String getInitialSearchUrl(String query) {
return String.format(SEARCH_URL, Uri.encode(query));
}
@Override
protected List<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
List<Manga> mangaList = new ArrayList<>();
for (Element currentHtmlBlock : parsedHtml.select("div.content_row")) {
Manga manga = constructPopularMangaFromHtml(currentHtmlBlock);
mangaList.add(manga);
}
return mangaList;
}
private Manga constructPopularMangaFromHtml(Element currentHtmlBlock) {
Manga manga = new Manga();
manga.source = getId();
Element urlElement = currentHtmlBlock.getElementsByTag("h2").select("a").first();
Element imgElement = currentHtmlBlock.getElementsByClass("manga_images").select("img").first();
if (urlElement != null) {
manga.setUrl(urlElement.attr("href"));
manga.title = urlElement.text();
}
if (imgElement != null) {
manga.thumbnail_url = BASE_URL + imgElement.attr("src");
}
return manga;
}
@Override
protected String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page) {
String path = Parser.href(parsedHtml, "a:contains(Вперед)");
return path != null ? POPULAR_MANGAS_URL + path : null;
}
@Override
protected List<Manga> parseSearchFromHtml(Document parsedHtml) {
return parsePopularMangasFromHtml(parsedHtml);
}
@Override
protected String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query) {
return null;
}
@Override
protected Manga parseHtmlToManga(String mangaUrl, String unparsedHtml) {
Document parsedDocument = Jsoup.parse(unparsedHtml);
Element infoElement = parsedDocument.getElementsByClass("mangatitle").first();
String description = parsedDocument.getElementById("description").text();
Manga manga = Manga.create(mangaUrl);
manga.author = infoElement.select("tr:eq(2) td:eq(1)").text();
manga.genre = infoElement.select("tr:eq(5) td:eq(1)").text();
manga.status = parseStatus(infoElement.select("tr:eq(4) td:eq(1)").text());
manga.description = description.replaceAll("Прислать описание", "");
manga.initialized = true;
return manga;
}
private int parseStatus(String status) {
if (status.contains("перевод продолжается")) {
return Manga.ONGOING;
} else if (status.contains("перевод завершен")) {
return Manga.COMPLETED;
} else return Manga.UNKNOWN;
}
@Override
protected List<Chapter> parseHtmlToChapters(String unparsedHtml) {
Document parsedDocument = Jsoup.parse(unparsedHtml);
List<Chapter> chapterList = new ArrayList<>();
for (Element chapterElement : parsedDocument.select("table.table_cha tr:gt(1)")) {
Chapter chapter = constructChapterFromHtmlBlock(chapterElement);
chapterList.add(chapter);
}
return chapterList;
}
private Chapter constructChapterFromHtmlBlock(Element chapterElement) {
Chapter chapter = Chapter.create();
Element urlElement = chapterElement.select("a").first();
String date = Parser.text(chapterElement, "div.date");
if (urlElement != null) {
chapter.name = urlElement.text();
chapter.url = urlElement.attr("href");
}
if (date != null) {
try {
chapter.date_upload = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH).parse(date).getTime();
} catch (ParseException e) { /* Ignore */ }
}
return chapter;
}
// Without this extra chapters are in the wrong place in the list
@Override
public void parseChapterNumber(Chapter chapter) {
// For chapters with url like /online/254903-fairy-tail_v56_ch474.html
String url = chapter.url.replace(".html", "");
Pattern pattern = Pattern.compile("\\d+_ch[\\d.]+");
Matcher matcher = pattern.matcher(url);
if (matcher.find()) {
String[] parts = matcher.group().split("_ch");
chapter.chapter_number = Float.parseFloat(parts[0] + "." + AddZero(parts[1]));
} else { // For chapters with url like /online/61216-3298.html
String name = chapter.name;
name = name.replaceAll("[\\s\\d\\w\\W]+v", "");
String volume = name.substring(0, name.indexOf(" - "));
String[] parts = name.replaceFirst("\\d+ - ", "").split(" ");
chapter.chapter_number = Float.parseFloat(volume + "." + AddZero(parts[0]));
}
}
private String AddZero(String num) {
if (Float.parseFloat(num) < 1000f) {
num = "0" + num.replace(".", "");
}
if (Float.parseFloat(num) < 100f) {
num = "0" + num.replace(".", "");
}
if (Float.parseFloat(num) < 10f) {
num = "0" + num.replace(".", "");
}
return num;
}
@Override
protected List<String> parseHtmlToPageUrls(String unparsedHtml) {
ArrayList<String> pages = new ArrayList<>();
int beginIndex = unparsedHtml.indexOf("fullimg\":[");
int endIndex = unparsedHtml.indexOf("]", beginIndex);
String trimmedHtml = unparsedHtml.substring(beginIndex + 10, endIndex);
trimmedHtml = trimmedHtml.replaceAll("\"", "");
String[] pageUrls = trimmedHtml.split(",");
for (int i = 0; i < pageUrls.length; i++) {
pages.add("");
}
return pages;
}
@Override
protected List<Page> parseFirstPage(List<? extends Page> pages, String unparsedHtml) {
int beginIndex = unparsedHtml.indexOf("fullimg\":[");
int endIndex = unparsedHtml.indexOf("]", beginIndex);
String trimmedHtml = unparsedHtml.substring(beginIndex + 10, endIndex);
trimmedHtml = trimmedHtml.replaceAll("\"", "");
String[] pageUrls = trimmedHtml.split(",");
for (int i = 0; i < pageUrls.length; i++) {
pages.get(i).setImageUrl(pageUrls[i].replaceAll("im.?\\.", ""));
}
return (List<Page>) pages;
}
@Override
protected String parseHtmlToImageUrl(String unparsedHtml) {
return null;
}
}

View File

@ -0,0 +1,95 @@
package eu.kanade.tachiyomi.data.source.online.russian
import android.content.Context
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.source.Language
import eu.kanade.tachiyomi.data.source.RU
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.SimpleDateFormat
import java.util.*
class Mangachan(context: Context, override val id: Int) : ParsedOnlineSource(context) {
override val name = "Mangachan"
override val baseUrl = "http://mangachan.ru"
override val lang: Language get() = RU
override fun popularMangaInitialUrl() = "$baseUrl/mostfavorites"
override fun searchMangaInitialUrl(query: String) = "$baseUrl/?do=search&subaction=search&story=$query"
override fun popularMangaSelector() = "div.content_row"
override fun popularMangaFromElement(element: Element, manga: Manga) {
element.select("h2 > a").first().let {
manga.setUrl(it.attr("href"))
manga.title = it.text()
}
element.select("img").first().let {
manga.thumbnail_url = baseUrl + it.attr("src")
}
}
override fun popularMangaNextPageSelector() = "a:contains(Вперед)"
override fun searchMangaSelector() = popularMangaSelector()
override fun searchMangaFromElement(element: Element, manga: Manga) {
popularMangaFromElement(element, manga)
}
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
override fun mangaDetailsParse(document: Document, manga: Manga) {
val infoElement = document.select("table.mangatitle").first()
val descElement = document.select("div#description").first()
manga.author = infoElement.select("tr:eq(2) > td:eq(1)").text()
manga.genre = infoElement.select("tr:eq(5) > td:eq(1)").text()
manga.status = parseStatus(infoElement.select("tr:eq(4) > td:eq(1)").text())
manga.description = descElement.textNodes().first().text()
}
private fun parseStatus(element: String): Int {
when {
element.contains("перевод завершен") -> return Manga.COMPLETED
element.contains("перевод продолжается") -> return Manga.ONGOING
else -> return Manga.UNKNOWN
}
}
override fun chapterListSelector() = "table.table_cha tr:gt(1)"
override fun chapterFromElement(element: Element, chapter: Chapter) {
val urlElement = element.select("a").first()
chapter.setUrl(urlElement.attr("href"))
chapter.name = urlElement.text()
chapter.date_upload = element.select("div.date").first()?.text()?.let {
SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(it).time
} ?: 0
}
override fun pageListParse(response: Response, pages: MutableList<Page>) {
val html = response.body().string()
val beginIndex = html.indexOf("fullimg\":[") + 10
val endIndex = html.indexOf(",]", beginIndex)
val trimmedHtml = html.substring(beginIndex, endIndex).replace("\"", "")
val pageUrls = trimmedHtml.split(',')
for ((i, url) in pageUrls.withIndex()) {
pages.add(Page(i, "", url))
}
}
override fun pageListParse(document: Document, pages: MutableList<Page>) { }
override fun imageUrlParse(document: Document) = ""
}

View File

@ -1,225 +0,0 @@
package eu.kanade.tachiyomi.data.source.online.russian;
import android.content.Context;
import android.net.Uri;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.source.Language;
import eu.kanade.tachiyomi.data.source.LanguageKt;
import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.data.source.model.MangasPage;
import eu.kanade.tachiyomi.data.source.model.Page;
import eu.kanade.tachiyomi.util.Parser;
public class Mintmanga extends Source {
public static final String NAME = "Mintmanga";
public static final String BASE_URL = "http://mintmanga.com";
public static final String POPULAR_MANGAS_URL = BASE_URL + "/list?sortType=rate";
public static final String SEARCH_URL = BASE_URL + "/search?q=%s";
public Mintmanga(Context context) {
super(context);
}
@Override
public Language getLang() {
return LanguageKt.getRU();
}
@Override
public String getName() {
return NAME;
}
@Override
public String getBaseUrl() {
return BASE_URL;
}
@Override
protected String getInitialPopularMangasUrl() {
return POPULAR_MANGAS_URL;
}
@Override
protected String getInitialSearchUrl(String query) {
return String.format(SEARCH_URL, Uri.encode(query));
}
@Override
protected List<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
List<Manga> mangaList = new ArrayList<>();
for (Element currentHtmlBlock : parsedHtml.select("div.desc")) {
Manga manga = constructPopularMangaFromHtml(currentHtmlBlock);
mangaList.add(manga);
}
return mangaList;
}
private Manga constructPopularMangaFromHtml(Element currentHtmlBlock) {
Manga manga = new Manga();
manga.source = getId();
Element urlElement = currentHtmlBlock.getElementsByTag("h3").select("a").first();
if (urlElement != null) {
manga.setUrl(urlElement.attr("href"));
manga.title = urlElement.text();
}
return manga;
}
@Override
protected String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page) {
String path = Parser.href(parsedHtml, "a:contains(→)");
return path != null ? BASE_URL + path : null;
}
@Override
protected List<Manga> parseSearchFromHtml(Document parsedHtml) {
return parsePopularMangasFromHtml(parsedHtml);
}
@Override
protected String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query) {
return null;
}
@Override
protected Manga parseHtmlToManga(String mangaUrl, String unparsedHtml) {
Document parsedDocument = Jsoup.parse(unparsedHtml);
Element infoElement = parsedDocument.select("div.leftContent").first();
Manga manga = Manga.create(mangaUrl);
manga.title = Parser.text(infoElement, "span.eng-name");
manga.author = Parser.text(infoElement, "span.elem_author ");
manga.genre = Parser.allText(infoElement, "span.elem_genre ").replaceAll(" ,", ",");
manga.description = Parser.allText(infoElement, "div.manga-description");
if (Parser.text(infoElement, "h1.names:contains(Сингл)") != null) {
manga.status = Manga.COMPLETED;
} else {
manga.status = parseStatus(Parser.text(infoElement, "p:has(b:contains(Перевод:))"));
}
String thumbnail = Parser.element(infoElement, "img").attr("data-full");
if (thumbnail != null) {
manga.thumbnail_url = thumbnail;
}
manga.initialized = true;
return manga;
}
private int parseStatus(String status) {
if (status.contains("продолжается")) {
return Manga.ONGOING;
}
if (status.contains("завершен")) {
return Manga.COMPLETED;
}
return Manga.UNKNOWN;
}
@Override
protected List<Chapter> parseHtmlToChapters(String unparsedHtml) {
Document parsedDocument = Jsoup.parse(unparsedHtml);
List<Chapter> chapterList = new ArrayList<>();
for (Element chapterElement : parsedDocument.select("div.chapters-link tbody tr")) {
Chapter chapter = constructChapterFromHtmlBlock(chapterElement);
chapterList.add(chapter);
}
return chapterList;
}
private Chapter constructChapterFromHtmlBlock(Element chapterElement) {
Chapter chapter = Chapter.create();
Element urlElement = Parser.element(chapterElement, "a");
String date = Parser.text(chapterElement, "td:eq(1)");
if (urlElement != null) {
chapter.setUrl(urlElement.attr("href") + "?mature=1");
chapter.name = urlElement.text().replaceAll(" новое", "");
}
if (date != null) {
try {
chapter.date_upload = new SimpleDateFormat("dd/MM/yy", Locale.ENGLISH).parse(date).getTime();
} catch (ParseException e) { /* Ignore */ }
}
return chapter;
}
// Without this extra chapters are in the wrong place in the list
@Override
public void parseChapterNumber(Chapter chapter) {
String url = chapter.url.replace("?mature=1", "");
String[] urlParts = url.replaceAll("/[\\w\\d]+/vol", "").split("/");
if (Float.parseFloat(urlParts[1]) < 1000f) {
urlParts[1] = "0" + urlParts[1];
}
if (Float.parseFloat(urlParts[1]) < 100f) {
urlParts[1] = "0" + urlParts[1];
}
if (Float.parseFloat(urlParts[1]) < 10f) {
urlParts[1] = "0" + urlParts[1];
}
chapter.chapter_number = Float.parseFloat(urlParts[0] + "." + urlParts[1]);
}
@Override
protected List<String> parseHtmlToPageUrls(String unparsedHtml) {
ArrayList<String> pages = new ArrayList<>();
int beginIndex = unparsedHtml.indexOf("rm_h.init( [");
int endIndex = unparsedHtml.indexOf("], 0, false);", beginIndex);
String trimmedHtml = unparsedHtml.substring(beginIndex + 13, endIndex);
trimmedHtml = trimmedHtml.replaceAll("[\"']", "");
String[] pageUrls = trimmedHtml.split("],\\[");
for (int i = 0; i < pageUrls.length; i++) {
pages.add("");
}
return pages;
}
@Override
protected List<Page> parseFirstPage(List<? extends Page> pages, String unparsedHtml) {
int beginIndex = unparsedHtml.indexOf("rm_h.init( [");
int endIndex = unparsedHtml.indexOf("], 0, false);", beginIndex);
String trimmedHtml = unparsedHtml.substring(beginIndex + 13, endIndex);
trimmedHtml = trimmedHtml.replaceAll("[\"']", "");
String[] pageUrls = trimmedHtml.split("],\\[");
for (int i = 0; i < pageUrls.length; i++) {
String[] urlParts = pageUrls[i].split(","); // auto/06/35,http://e4.adultmanga.me/,/55/01.png
String page = urlParts[1] + urlParts[0] + urlParts[2];
pages.get(i).setImageUrl(page);
}
return (List<Page>) pages;
}
@Override
protected String parseHtmlToImageUrl(String unparsedHtml) {
return null;
}
}

View File

@ -0,0 +1,102 @@
package eu.kanade.tachiyomi.data.source.online.russian
import android.content.Context
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.source.Language
import eu.kanade.tachiyomi.data.source.RU
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.SimpleDateFormat
import java.util.*
import java.util.regex.Pattern
class Mintmanga(context: Context, override val id: Int) : ParsedOnlineSource(context) {
override val name = "Mintmanga"
override val baseUrl = "http://mintmanga.com"
override val lang: Language get() = RU
override fun popularMangaInitialUrl() = "$baseUrl/list?sortType=rate"
override fun searchMangaInitialUrl(query: String) = "$baseUrl/search?q=$query"
override fun popularMangaSelector() = "div.desc"
override fun popularMangaFromElement(element: Element, manga: Manga) {
element.select("h3 > a").first().let {
manga.setUrl(it.attr("href"))
manga.title = it.attr("title")
}
}
override fun popularMangaNextPageSelector() = "a.nextLink"
override fun searchMangaSelector() = popularMangaSelector()
override fun searchMangaFromElement(element: Element, manga: Manga) {
popularMangaFromElement(element, manga)
}
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
override fun mangaDetailsParse(document: Document, manga: Manga) {
val infoElement = document.select("div.leftContent").first()
manga.author = infoElement.select("span.elem_author").first()?.text()
manga.genre = infoElement.select("span.elem_genre").text().replace(" ,", ",")
manga.description = infoElement.select("div.manga-description").text()
manga.status = parseStatus(infoElement.html())
manga.thumbnail_url = infoElement.select("img").attr("data-full")
}
private fun parseStatus(element: String): Int {
when {
element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> return Manga.LICENSED
element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> return Manga.COMPLETED
element.contains("<b>Перевод:</b> продолжается") -> return Manga.ONGOING
else -> return Manga.UNKNOWN
}
}
override fun chapterListSelector() = "div.chapters-link tbody tr"
override fun chapterFromElement(element: Element, chapter: Chapter) {
val urlElement = element.select("a").first()
chapter.setUrl(urlElement.attr("href") + "?mature=1")
chapter.name = urlElement.text().replace(" новое", "")
chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
SimpleDateFormat("dd/MM/yy", Locale.US).parse(it).time
} ?: 0
}
override fun parseChapterNumber(chapter: Chapter) {
chapter.chapter_number = -2f
}
override fun pageListParse(response: Response, pages: MutableList<Page>) {
val html = response.body().string()
val beginIndex = html.indexOf("rm_h.init( [")
val endIndex = html.indexOf("], 0, false);", beginIndex)
val trimmedHtml = html.substring(beginIndex, endIndex).replace("[\"\']+".toRegex(), "")
val p = Pattern.compile("auto/[\\w/]+,http://[\\w.]+/,/[\\w./]+.(png|jpg)+")
val m = p.matcher(trimmedHtml)
var i = 0
while (m.find()) {
val urlParts = m.group().split(',')
pages.add(Page(i++, "", urlParts[1] + urlParts[0] + urlParts[2]))
}
}
override fun pageListParse(document: Document, pages: MutableList<Page>) { }
override fun imageUrlParse(document: Document) = ""
}

View File

@ -1,225 +0,0 @@
package eu.kanade.tachiyomi.data.source.online.russian;
import android.content.Context;
import android.net.Uri;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.source.Language;
import eu.kanade.tachiyomi.data.source.LanguageKt;
import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.data.source.model.MangasPage;
import eu.kanade.tachiyomi.data.source.model.Page;
import eu.kanade.tachiyomi.util.Parser;
public class Readmanga extends Source {
public static final String NAME = "Readmanga";
public static final String BASE_URL = "http://readmanga.me";
public static final String POPULAR_MANGAS_URL = BASE_URL + "/list?sortType=rate";
public static final String SEARCH_URL = BASE_URL + "/search?q=%s";
public Readmanga(Context context) {
super(context);
}
@Override
public Language getLang() {
return LanguageKt.getRU();
}
@Override
public String getName() {
return NAME;
}
@Override
public String getBaseUrl() {
return BASE_URL;
}
@Override
protected String getInitialPopularMangasUrl() {
return POPULAR_MANGAS_URL;
}
@Override
protected String getInitialSearchUrl(String query) {
return String.format(SEARCH_URL, Uri.encode(query));
}
@Override
protected List<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
List<Manga> mangaList = new ArrayList<>();
for (Element currentHtmlBlock : parsedHtml.select("div.desc")) {
Manga manga = constructPopularMangaFromHtml(currentHtmlBlock);
mangaList.add(manga);
}
return mangaList;
}
private Manga constructPopularMangaFromHtml(Element currentHtmlBlock) {
Manga manga = new Manga();
manga.source = getId();
Element urlElement = currentHtmlBlock.getElementsByTag("h3").select("a").first();
if (urlElement != null) {
manga.setUrl(urlElement.attr("href"));
manga.title = urlElement.text();
}
return manga;
}
@Override
protected String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page) {
String path = Parser.href(parsedHtml, "a:contains(→)");
return path != null ? BASE_URL + path : null;
}
@Override
protected List<Manga> parseSearchFromHtml(Document parsedHtml) {
return parsePopularMangasFromHtml(parsedHtml);
}
@Override
protected String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query) {
return null;
}
@Override
protected Manga parseHtmlToManga(String mangaUrl, String unparsedHtml) {
Document parsedDocument = Jsoup.parse(unparsedHtml);
Element infoElement = parsedDocument.select("div.leftContent").first();
Manga manga = Manga.create(mangaUrl);
manga.title = Parser.text(infoElement, "span.eng-name");
manga.author = Parser.text(infoElement, "span.elem_author ");
manga.genre = Parser.allText(infoElement, "span.elem_genre ").replaceAll(" ,", ",");
manga.description = Parser.allText(infoElement, "div.manga-description");
if (Parser.text(infoElement, "h1.names:contains(Сингл)") != null) {
manga.status = Manga.COMPLETED;
} else {
manga.status = parseStatus(Parser.text(infoElement, "p:has(b:contains(Перевод:))"));
}
String thumbnail = Parser.element(infoElement, "img").attr("data-full");
if (thumbnail != null) {
manga.thumbnail_url = thumbnail;
}
manga.initialized = true;
return manga;
}
private int parseStatus(String status) {
if (status.contains("продолжается")) {
return Manga.ONGOING;
}
if (status.contains("завершен")) {
return Manga.COMPLETED;
}
return Manga.UNKNOWN;
}
@Override
protected List<Chapter> parseHtmlToChapters(String unparsedHtml) {
Document parsedDocument = Jsoup.parse(unparsedHtml);
List<Chapter> chapterList = new ArrayList<>();
for (Element chapterElement : parsedDocument.select("div.chapters-link tbody tr")) {
Chapter chapter = constructChapterFromHtmlBlock(chapterElement);
chapterList.add(chapter);
}
return chapterList;
}
private Chapter constructChapterFromHtmlBlock(Element chapterElement) {
Chapter chapter = Chapter.create();
Element urlElement = Parser.element(chapterElement, "a");
String date = Parser.text(chapterElement, "td:eq(1)");
if (urlElement != null) {
chapter.setUrl(urlElement.attr("href") + "?mature=1");
chapter.name = urlElement.text().replaceAll(" новое", "");
}
if (date != null) {
try {
chapter.date_upload = new SimpleDateFormat("dd/MM/yy", Locale.ENGLISH).parse(date).getTime();
} catch (ParseException e) { /* Ignore */ }
}
return chapter;
}
// Without this extra chapters are in the wrong place in the list
@Override
public void parseChapterNumber(Chapter chapter) {
String url = chapter.url.replace("?mature=1", "");
String[] urlParts = url.replaceAll("/[\\w\\d]+/vol", "").split("/");
if (Float.parseFloat(urlParts[1]) < 1000f) {
urlParts[1] = "0" + urlParts[1];
}
if (Float.parseFloat(urlParts[1]) < 100f) {
urlParts[1] = "0" + urlParts[1];
}
if (Float.parseFloat(urlParts[1]) < 10f) {
urlParts[1] = "0" + urlParts[1];
}
chapter.chapter_number = Float.parseFloat(urlParts[0] + "." + urlParts[1]);
}
@Override
protected List<String> parseHtmlToPageUrls(String unparsedHtml) {
ArrayList<String> pages = new ArrayList<>();
int beginIndex = unparsedHtml.indexOf("rm_h.init( [");
int endIndex = unparsedHtml.indexOf("], 0, false);", beginIndex);
String trimmedHtml = unparsedHtml.substring(beginIndex + 13, endIndex);
trimmedHtml = trimmedHtml.replaceAll("[\"']", "");
String[] pageUrls = trimmedHtml.split("],\\[");
for (int i = 0; i < pageUrls.length; i++) {
pages.add("");
}
return pages;
}
@Override
protected List<Page> parseFirstPage(List<? extends Page> pages, String unparsedHtml) {
int beginIndex = unparsedHtml.indexOf("rm_h.init( [");
int endIndex = unparsedHtml.indexOf("], 0, false);", beginIndex);
String trimmedHtml = unparsedHtml.substring(beginIndex + 13, endIndex);
trimmedHtml = trimmedHtml.replaceAll("[\"']", "");
String[] pageUrls = trimmedHtml.split("],\\[");
for (int i = 0; i < pageUrls.length; i++) {
String[] urlParts = pageUrls[i].split(","); // auto/12/56,http://e7.postfact.ru/,/51/01.jpg_res.jpg
String page = urlParts[1] + urlParts[0] + urlParts[2];
pages.get(i).setImageUrl(page);
}
return (List<Page>) pages;
}
@Override
protected String parseHtmlToImageUrl(String unparsedHtml) {
return null;
}
}

View File

@ -0,0 +1,102 @@
package eu.kanade.tachiyomi.data.source.online.russian
import android.content.Context
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.source.Language
import eu.kanade.tachiyomi.data.source.RU
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.SimpleDateFormat
import java.util.*
import java.util.regex.Pattern
class Readmanga(context: Context, override val id: Int) : ParsedOnlineSource(context) {
override val name = "Readmanga"
override val baseUrl = "http://readmanga.me"
override val lang: Language get() = RU
override fun popularMangaInitialUrl() = "$baseUrl/list?sortType=rate"
override fun searchMangaInitialUrl(query: String) = "$baseUrl/search?q=$query"
override fun popularMangaSelector() = "div.desc"
override fun popularMangaFromElement(element: Element, manga: Manga) {
element.select("h3 > a").first().let {
manga.setUrl(it.attr("href"))
manga.title = it.attr("title")
}
}
override fun popularMangaNextPageSelector() = "a.nextLink"
override fun searchMangaSelector() = popularMangaSelector()
override fun searchMangaFromElement(element: Element, manga: Manga) {
popularMangaFromElement(element, manga)
}
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
override fun mangaDetailsParse(document: Document, manga: Manga) {
val infoElement = document.select("div.leftContent").first()
manga.author = infoElement.select("span.elem_author").first()?.text()
manga.genre = infoElement.select("span.elem_genre").text().replace(" ,", ",")
manga.description = infoElement.select("div.manga-description").text()
manga.status = parseStatus(infoElement.html())
manga.thumbnail_url = infoElement.select("img").attr("data-full")
}
private fun parseStatus(element: String): Int {
when {
element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> return Manga.LICENSED
element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> return Manga.COMPLETED
element.contains("<b>Перевод:</b> продолжается") -> return Manga.ONGOING
else -> return Manga.UNKNOWN
}
}
override fun chapterListSelector() = "div.chapters-link tbody tr"
override fun chapterFromElement(element: Element, chapter: Chapter) {
val urlElement = element.select("a").first()
chapter.setUrl(urlElement.attr("href") + "?mature=1")
chapter.name = urlElement.text().replace(" новое", "")
chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
SimpleDateFormat("dd/MM/yy", Locale.US).parse(it).time
} ?: 0
}
override fun parseChapterNumber(chapter: Chapter) {
chapter.chapter_number = -2f
}
override fun pageListParse(response: Response, pages: MutableList<Page>) {
val html = response.body().string()
val beginIndex = html.indexOf("rm_h.init( [")
val endIndex = html.indexOf("], 0, false);", beginIndex)
val trimmedHtml = html.substring(beginIndex, endIndex).replace("[\"\']+".toRegex(), "")
val p = Pattern.compile("auto/[\\w/]+,http://[\\w.]+/,/[\\w./]+.(png|jpg)+")
val m = p.matcher(trimmedHtml)
var i = 0
while (m.find()) {
val urlParts = m.group().split(',')
pages.add(Page(i++, "", urlParts[1] + urlParts[0] + urlParts[2]))
}
}
override fun pageListParse(document: Document, pages: MutableList<Page>) { }
override fun imageUrlParse(document: Document) = ""
}

View File

@ -1,99 +0,0 @@
package eu.kanade.tachiyomi.data.updater;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import javax.inject.Inject;
import eu.kanade.tachiyomi.App;
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
public class UpdateDownloader extends AsyncTask<String, Void, Void> {
/**
* Name of cache directory.
*/
private static final String PARAMETER_CACHE_DIRECTORY = "apk_downloads";
/**
* Interface to global information about an application environment.
*/
private final Context context;
/**
* Cache directory used for cache management.
*/
private final File cacheDir;
@Inject PreferencesHelper preferencesHelper;
/**
* Constructor of UpdaterCache.
*
* @param context application environment interface.
*/
public UpdateDownloader(Context context) {
App.get(context).getComponent().inject(this);
this.context = context;
// Get cache directory from parameter.
cacheDir = new File(preferencesHelper.getDownloadsDirectory(), PARAMETER_CACHE_DIRECTORY);
// Create cache directory.
createCacheDir();
}
/**
* Create cache directory if it doesn't exist
*
* @return true if cache dir is created otherwise false.
*/
@SuppressWarnings("UnusedReturnValue")
private boolean createCacheDir() {
return !cacheDir.exists() && cacheDir.mkdirs();
}
@Override
protected Void doInBackground(String... args) {
try {
createCacheDir();
URL url = new URL(args[0]);
HttpURLConnection c = (HttpURLConnection) url.openConnection();
c.connect();
File outputFile = new File(cacheDir, "update.apk");
if (outputFile.exists()) {
//noinspection ResultOfMethodCallIgnored
outputFile.delete();
}
FileOutputStream fos = new FileOutputStream(outputFile);
InputStream is = c.getInputStream();
byte[] buffer = new byte[1024];
int len1;
while ((len1 = is.read(buffer)) != -1) {
fos.write(buffer, 0, len1);
}
fos.close();
is.close();
// Prompt install interface
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(outputFile), "application/vnd.android.package-archive");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // without this flag android returned a intent error!
context.startActivity(intent);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}

View File

@ -0,0 +1,206 @@
package eu.kanade.tachiyomi.data.updater
import android.app.Notification
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.AsyncTask
import android.support.v4.app.NotificationCompat
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.Constants
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.network.GET
import eu.kanade.tachiyomi.data.network.NetworkHelper
import eu.kanade.tachiyomi.data.network.ProgressListener
import eu.kanade.tachiyomi.data.network.newCallWithProgress
import eu.kanade.tachiyomi.util.notificationManager
import eu.kanade.tachiyomi.util.saveTo
import timber.log.Timber
import java.io.File
import javax.inject.Inject
class UpdateDownloader(private val context: Context) :
AsyncTask<String, Int, UpdateDownloader.DownloadResult>() {
companion object {
/**
* Prompt user with apk install intent
* @param context context
* @param file file of apk that is installed
*/
fun installAPK(context: Context, file: File) {
// Prompt install interface
val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive")
// Without this flag android returned a intent error!
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(intent)
}
}
@Inject lateinit var network: NetworkHelper
/**
* Default download dir
*/
private val apkFile = File(context.externalCacheDir, "update.apk")
/**
* Notification builder
*/
private val notificationBuilder = NotificationCompat.Builder(context)
/**
* Id of the notification
*/
private val notificationId: Int
get() = Constants.NOTIFICATION_UPDATER_ID
init {
App.get(context).component.inject(this)
}
/**
* Class containing download result
* @param url url of file
* @param successful status of download
*/
class DownloadResult(var url: String, var successful: Boolean)
/**
* Called before downloading
*/
override fun onPreExecute() {
// Create download notification
with(notificationBuilder) {
setContentTitle(context.getString(R.string.update_check_notification_file_download))
setContentText(context.getString(R.string.update_check_notification_download_in_progress))
setSmallIcon(android.R.drawable.stat_sys_download)
}
}
override fun doInBackground(vararg params: String?): DownloadResult {
// Initialize information array containing path and url to file.
val result = DownloadResult(params[0]!!, false)
// Progress of the download
var savedProgress = 0
val progressListener = object : ProgressListener {
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
val progress = (100 * bytesRead / contentLength).toInt()
if (progress > savedProgress) {
savedProgress = progress
publishProgress(progress)
}
}
}
try {
// Make the request and download the file
val response = network.client.newCallWithProgress(GET(result.url), progressListener).execute()
if (response.isSuccessful) {
response.body().source().saveTo(apkFile)
// Set download successful
result.successful = true
} else {
response.close()
}
} catch (e: Exception) {
Timber.e(e, e.message)
}
return result
}
/**
* Called when progress is updated
* @param values values containing progress
*/
override fun onProgressUpdate(vararg values: Int?) {
// Notify notification manager to update notification
values.getOrNull(0)?.let {
notificationBuilder.setProgress(100, it, false)
// Displays the progress bar on notification
context.notificationManager.notify(notificationId, notificationBuilder.build())
}
}
/**
* Called when download done
* @param result string containing download information
*/
override fun onPostExecute(result: DownloadResult) {
with(notificationBuilder) {
if (result.successful) {
setContentTitle(context.getString(R.string.app_name))
setContentText(context.getString(R.string.update_check_notification_download_complete))
addAction(R.drawable.ic_system_update_grey_24dp_img, context.getString(R.string.action_install),
getInstallOnReceivedIntent(InstallOnReceived.INSTALL_APK, apkFile.absolutePath))
addAction(R.drawable.ic_clear_grey_24dp_img, context.getString(R.string.action_cancel),
getInstallOnReceivedIntent(InstallOnReceived.CANCEL_NOTIFICATION))
} else {
setContentText(context.getString(R.string.update_check_notification_download_error))
addAction(R.drawable.ic_refresh_grey_24dp_img, context.getString(R.string.action_retry),
getInstallOnReceivedIntent(InstallOnReceived.RETRY_DOWNLOAD, result.url))
addAction(R.drawable.ic_clear_grey_24dp_img, context.getString(R.string.action_cancel),
getInstallOnReceivedIntent(InstallOnReceived.CANCEL_NOTIFICATION))
}
setSmallIcon(android.R.drawable.stat_sys_download_done)
setProgress(0, 0, false)
}
val notification = notificationBuilder.build()
notification.flags = Notification.FLAG_NO_CLEAR
context.notificationManager.notify(notificationId, notification)
}
/**
* Returns broadcast intent
* @param action action name of broadcast intent
* @param path path of file | url of file
* @return broadcast intent
*/
fun getInstallOnReceivedIntent(action: String, path: String = ""): PendingIntent {
val intent = Intent(context, InstallOnReceived::class.java).apply {
this.action = action
putExtra(InstallOnReceived.FILE_LOCATION, path)
}
return PendingIntent.getBroadcast(context, 0, intent, 0)
}
/**
* BroadcastEvent used to install apk or retry download
*/
class InstallOnReceived : BroadcastReceiver() {
companion object {
// Install apk action
const val INSTALL_APK = "eu.kanade.INSTALL_APK"
// Retry download action
const val RETRY_DOWNLOAD = "eu.kanade.RETRY_DOWNLOAD"
// Retry download action
const val CANCEL_NOTIFICATION = "eu.kanade.CANCEL_NOTIFICATION"
// Absolute path of file || URL of file
const val FILE_LOCATION = "file_location"
}
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
// Install apk.
INSTALL_APK -> UpdateDownloader.installAPK(context, File(intent.getStringExtra(FILE_LOCATION)))
// Retry download.
RETRY_DOWNLOAD -> UpdateDownloader(context).execute(intent.getStringExtra(FILE_LOCATION))
CANCEL_NOTIFICATION -> context.notificationManager.cancel(Constants.NOTIFICATION_UPDATER_ID)
}
}
}
}

View File

@ -0,0 +1,109 @@
package eu.kanade.tachiyomi.data.updater
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.SystemClock
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.DeviceUtil
import eu.kanade.tachiyomi.util.alarmManager
import eu.kanade.tachiyomi.util.notification
import eu.kanade.tachiyomi.util.notificationManager
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
class UpdateDownloaderAlarm : BroadcastReceiver() {
companion object {
const val CHECK_UPDATE_ACTION = "eu.kanade.CHECK_UPDATE"
/**
* Sets the alarm to run the intent that checks for update
* @param context the application context.
* @param intervalInHours the time in hours when it will be executed.
*/
@JvmStatic
@JvmOverloads
fun startAlarm(context: Context, intervalInHours: Int = 12, isEnabled: Boolean = PreferencesHelper.getAutomaticUpdateStatus(context)) {
// Stop previous running alarms if needed, and do not restart it if the interval is 0.
UpdateDownloaderAlarm.stopAlarm(context)
if (intervalInHours == 0 || !isEnabled)
return
// Get the time the alarm should fire the event to update.
val intervalInMillis = intervalInHours * 60 * 60 * 1000
val nextRun = SystemClock.elapsedRealtime() + intervalInMillis
// Start the alarm.
val pendingIntent = getPendingIntent(context)
context.alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
nextRun, intervalInMillis.toLong(), pendingIntent)
}
/**
* Stops the alarm if it's running.
* @param context the application context.
*/
fun stopAlarm(context: Context) {
val pendingIntent = getPendingIntent(context)
context.alarmManager.cancel(pendingIntent)
}
/**
* Returns broadcast intent
* @param context the application context.
* @return broadcast intent
*/
fun getPendingIntent(context: Context): PendingIntent {
return PendingIntent.getBroadcast(context, 0,
Intent(context, UpdateDownloaderAlarm::class.java).apply {
this.action = CHECK_UPDATE_ACTION
}, PendingIntent.FLAG_UPDATE_CURRENT)
}
}
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
// Start the alarm when the system is booted.
Intent.ACTION_BOOT_COMPLETED -> startAlarm(context)
// Update the library when the alarm fires an event.
CHECK_UPDATE_ACTION -> checkVersion(context)
}
}
fun checkVersion(context: Context) {
if (DeviceUtil.isNetworkConnected(context)) {
val updateChecker = GithubUpdateChecker(context)
updateChecker.checkForApplicationUpdate()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ release ->
//Get version of latest release
var newVersion = release.version
newVersion = newVersion.replace("[^\\d.]".toRegex(), "")
//Check if latest version is different from current version
if (newVersion != BuildConfig.VERSION_NAME) {
val downloadLink = release.downloadLink
val n = context.notification() {
setContentTitle(context.getString(R.string.update_check_notification_update_available))
addAction(android.R.drawable.stat_sys_download_done, context.getString(eu.kanade.tachiyomi.R.string.action_download),
UpdateDownloader(context).getInstallOnReceivedIntent(UpdateDownloader.InstallOnReceived.RETRY_DOWNLOAD, downloadLink))
setSmallIcon(android.R.drawable.stat_sys_download_done)
}
// Displays the progress bar on notification
context.notificationManager.notify(0, n);
}
}, {
it.printStackTrace()
})
}
}
}

View File

@ -1,6 +0,0 @@
package eu.kanade.tachiyomi.event
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
class DownloadChaptersEvent(val manga: Manga, val chapters: List<Chapter>)

View File

@ -3,10 +3,13 @@ package eu.kanade.tachiyomi.injection.component
import android.app.Application
import dagger.Component
import eu.kanade.tachiyomi.data.download.DownloadService
import eu.kanade.tachiyomi.data.glide.AppGlideModule
import eu.kanade.tachiyomi.data.glide.MangaModelLoader
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
import eu.kanade.tachiyomi.data.mangasync.UpdateMangaSyncService
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService
import eu.kanade.tachiyomi.data.source.base.Source
import eu.kanade.tachiyomi.data.source.Source
import eu.kanade.tachiyomi.data.source.online.OnlineSource
import eu.kanade.tachiyomi.data.updater.UpdateDownloader
import eu.kanade.tachiyomi.injection.module.AppModule
import eu.kanade.tachiyomi.injection.module.DataModule
@ -15,7 +18,7 @@ import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
import eu.kanade.tachiyomi.ui.category.CategoryPresenter
import eu.kanade.tachiyomi.ui.download.DownloadPresenter
import eu.kanade.tachiyomi.ui.library.LibraryPresenter
import eu.kanade.tachiyomi.ui.manga.MangaActivity
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaPresenter
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersPresenter
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoPresenter
@ -41,16 +44,21 @@ interface AppComponent {
fun inject(recentChaptersPresenter: RecentChaptersPresenter)
fun inject(backupPresenter: BackupPresenter)
fun inject(mangaActivity: MangaActivity)
fun inject(mainActivity: MainActivity)
fun inject(settingsActivity: SettingsActivity)
fun inject(source: Source)
fun inject(mangaSyncService: MangaSyncService)
fun inject(onlineSource: OnlineSource)
fun inject(libraryUpdateService: LibraryUpdateService)
fun inject(downloadService: DownloadService)
fun inject(updateMangaSyncService: UpdateMangaSyncService)
fun inject(mangaModelLoader: MangaModelLoader)
fun inject(appGlideModule: AppGlideModule)
fun inject(updateDownloader: UpdateDownloader)
fun application(): Application

View File

@ -10,6 +10,7 @@ import android.view.View
import android.view.ViewGroup
import com.afollestad.materialdialogs.MaterialDialog
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.activity.ActivityMixin
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
import eu.kanade.tachiyomi.util.toast
import kotlinx.android.synthetic.main.fragment_backup.*
@ -40,7 +41,7 @@ class BackupFragment : BaseRxFragment<BackupPresenter>() {
}
override fun onViewCreated(view: View, savedState: Bundle?) {
baseActivity.requestPermissionsOnMarshmallow()
(activity as ActivityMixin).requestPermissionsOnMarshmallow()
subscriptions = SubscriptionList()
backup_button.setOnClickListener {
@ -57,7 +58,7 @@ class BackupFragment : BaseRxFragment<BackupPresenter>() {
restore_button.setOnClickListener {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "application/octet-stream"
intent.type = "application/*"
val chooser = Intent.createChooser(intent, getString(R.string.file_select_backup))
startActivityForResult(chooser, REQUEST_BACKUP_OPEN)
}
@ -76,7 +77,7 @@ class BackupFragment : BaseRxFragment<BackupPresenter>() {
fun onBackupCompleted(file: File) {
dismissBackupDialog()
val intent = Intent(Intent.ACTION_SEND)
intent.type = "text/plain"
intent.type = "application/json"
intent.putExtra(Intent.EXTRA_STREAM, Uri.parse("file://" + file))
startActivity(Intent.createChooser(intent, ""))
}

View File

@ -0,0 +1,75 @@
package eu.kanade.tachiyomi.ui.base.activity
import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import android.support.v4.app.ActivityCompat
import android.support.v4.content.ContextCompat
import android.support.v7.app.ActionBar
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.R
interface ActivityMixin {
fun setupToolbar(toolbar: Toolbar, backNavigation: Boolean = true) {
setSupportActionBar(toolbar)
getSupportActionBar()?.setDisplayHomeAsUpEnabled(true)
if (backNavigation) {
toolbar.setNavigationOnClickListener { onBackPressed() }
}
}
fun setAppTheme() {
setTheme(when (App.get(getActivity()).appTheme) {
2 -> R.style.Theme_Tachiyomi_Dark
else -> R.style.Theme_Tachiyomi
})
}
fun setToolbarTitle(title: String) {
getSupportActionBar()?.title = title
}
fun setToolbarTitle(titleResource: Int) {
getSupportActionBar()?.title = getString(titleResource)
}
fun setToolbarSubtitle(title: String) {
getSupportActionBar()?.subtitle = title
}
fun setToolbarSubtitle(titleResource: Int) {
getSupportActionBar()?.subtitle = getString(titleResource)
}
/**
* Requests read and write permissions on Android M and higher.
*/
fun requestPermissionsOnMarshmallow() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (ContextCompat.checkSelfPermission(getActivity(),
Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(getActivity(),
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE),
1)
}
}
}
fun getActivity(): AppCompatActivity
fun onBackPressed()
fun getSupportActionBar(): ActionBar?
fun setSupportActionBar(toolbar: Toolbar?)
fun setTheme(resource: Int)
fun getString(resource: Int): String
}

View File

@ -1,77 +1,9 @@
package eu.kanade.tachiyomi.ui.base.activity
import android.Manifest
import android.content.pm.PackageManager
import android.graphics.Color
import android.os.Build
import android.support.design.widget.Snackbar
import android.support.v4.app.ActivityCompat
import android.support.v4.content.ContextCompat
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.view.View
import android.widget.TextView
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.R
open class BaseActivity : AppCompatActivity() {
abstract class BaseActivity : AppCompatActivity(), ActivityMixin {
protected fun setupToolbar(toolbar: Toolbar, backNavigation: Boolean = true) {
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
if (backNavigation) {
toolbar.setNavigationOnClickListener { onBackPressed() }
}
}
override fun getActivity() = this
fun setAppTheme() {
when (app.appTheme) {
2 -> setTheme(R.style.Theme_Tachiyomi_Dark)
else -> setTheme(R.style.Theme_Tachiyomi)
}
}
fun setToolbarTitle(title: String) {
supportActionBar?.title = title
}
fun setToolbarTitle(titleResource: Int) {
supportActionBar?.title = getString(titleResource)
}
fun setToolbarSubtitle(title: String) {
supportActionBar?.subtitle = title
}
fun setToolbarSubtitle(titleResource: Int) {
supportActionBar?.subtitle = getString(titleResource)
}
/**
* Requests read and write permissions on Android M and higher.
*/
fun requestPermissionsOnMarshmallow() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE),
1)
}
}
}
protected val app: App
get() = App.get(this)
inline fun View.snack(message: String, length: Int = Snackbar.LENGTH_LONG, f: Snackbar.() -> Unit) {
val snack = Snackbar.make(this, message, length)
val textView = snack.view.findViewById(android.support.design.R.id.snackbar_text) as TextView
textView.setTextColor(Color.WHITE)
snack.f()
snack.show()
}
}
}

View File

@ -1,94 +0,0 @@
package eu.kanade.tachiyomi.ui.base.activity;
import android.os.Bundle;
import android.support.annotation.NonNull;
import eu.kanade.tachiyomi.App;
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
import nucleus.factory.PresenterFactory;
import nucleus.factory.ReflectionPresenterFactory;
import nucleus.presenter.Presenter;
import nucleus.view.PresenterLifecycleDelegate;
import nucleus.view.ViewWithPresenter;
/**
* This class is an example of how an activity could controls it's presenter.
* You can inherit from this class or copy/paste this class's code to
* create your own view implementation.
*
* @param <P> a type of presenter to return with {@link #getPresenter}.
*/
public abstract class BaseRxActivity<P extends Presenter> extends BaseActivity implements ViewWithPresenter<P> {
private static final String PRESENTER_STATE_KEY = "presenter_state";
private PresenterLifecycleDelegate<P> presenterDelegate =
new PresenterLifecycleDelegate<>(ReflectionPresenterFactory.<P>fromViewClass(getClass()));
/**
* Returns a current presenter factory.
*/
public PresenterFactory<P> getPresenterFactory() {
return presenterDelegate.getPresenterFactory();
}
/**
* Sets a presenter factory.
* Call this method before onCreate/onFinishInflate to override default {@link ReflectionPresenterFactory} presenter factory.
* Use this method for presenter dependency injection.
*/
@Override
public void setPresenterFactory(PresenterFactory<P> presenterFactory) {
presenterDelegate.setPresenterFactory(presenterFactory);
}
/**
* Returns a current attached presenter.
* This method is guaranteed to return a non-null value between
* onResume/onPause and onAttachedToWindow/onDetachedFromWindow calls
* if the presenter factory returns a non-null value.
*
* @return a currently attached presenter or null.
*/
public P getPresenter() {
return presenterDelegate.getPresenter();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
final PresenterFactory<P> superFactory = getPresenterFactory();
setPresenterFactory(new PresenterFactory<P>() {
@Override
public P createPresenter() {
P presenter = superFactory.createPresenter();
App app = (App) getApplication();
app.getComponentReflection().inject(presenter);
((BasePresenter) presenter).setContext(app.getApplicationContext());
return presenter;
}
});
super.onCreate(savedInstanceState);
if (savedInstanceState != null)
presenterDelegate.onRestoreInstanceState(savedInstanceState.getBundle(PRESENTER_STATE_KEY));
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBundle(PRESENTER_STATE_KEY, presenterDelegate.onSaveInstanceState());
}
@Override
protected void onResume() {
super.onResume();
presenterDelegate.onResume(this);
}
@Override
protected void onPause() {
super.onPause();
presenterDelegate.onPause(isFinishing());
}
}

Some files were not shown because too many files have changed in this diff Show More