Refactor tracker and add tests

This commit is contained in:
Koitharu
2022-06-16 15:26:57 +03:00
parent 3edfd0892a
commit c82bacb037
30 changed files with 1070 additions and 308 deletions

1
.gitignore vendored
View File

@@ -12,6 +12,7 @@
/.idea/assetWizardSettings.xml /.idea/assetWizardSettings.xml
/.idea/kotlinScripting.xml /.idea/kotlinScripting.xml
/.idea/deploymentTargetDropDown.xml /.idea/deploymentTargetDropDown.xml
/.idea/androidTestResultsUserPreferences.xml
.DS_Store .DS_Store
/build /build
/captures /captures

View File

@@ -116,11 +116,16 @@ dependencies {
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.2' testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.2'
testImplementation 'io.insert-koin:koin-test-junit4:3.2.0'
androidTestImplementation 'androidx.test:runner:1.4.0' androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test:rules:1.4.0' androidTestImplementation 'androidx.test:rules:1.4.0'
androidTestImplementation 'androidx.test:core-ktx:1.4.0' androidTestImplementation 'androidx.test:core-ktx:1.4.0'
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.3' androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.3'
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.2'
androidTestImplementation 'io.insert-koin:koin-test:3.2.0'
androidTestImplementation 'io.insert-koin:koin-test-junit4:3.2.0'
androidTestImplementation 'androidx.room:room-testing:2.4.2' androidTestImplementation 'androidx.room:room-testing:2.4.2'
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.13.0'
} }

View File

@@ -0,0 +1,163 @@
{
"id": -2096681732556647985,
"title": "Странствия Эманон",
"url": "/stranstviia_emanon",
"publicUrl": "https://readmanga.io/stranstviia_emanon",
"rating": 0.9400894,
"isNsfw": true,
"coverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_p.jpg",
"tags": [
{
"title": "Сверхъестественное",
"key": "supernatural",
"source": "READMANGA_RU"
},
{
"title": "Сэйнэн",
"key": "seinen",
"source": "READMANGA_RU"
},
{
"title": "Повседневность",
"key": "slice_of_life",
"source": "READMANGA_RU"
},
{
"title": "Приключения",
"key": "adventure",
"source": "READMANGA_RU"
}
],
"state": "FINISHED",
"largeCoverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_o.jpg",
"description": "Продолжение истории о загадочной девушке по имени Эманон, которая помнит всё, что происходило на Земле за последние три миллиарда лет. \n<br>Начало истории читайте в \"Воспоминаниях Эманон\". \n<div class=\"clearfix\"></div>",
"chapters": [
{
"id": 1552943969433540704,
"name": "1 - 1",
"number": 1,
"url": "/stranstviia_emanon/vol1/1",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 1552943969433540705,
"name": "1 - 2",
"number": 2,
"url": "/stranstviia_emanon/vol1/2",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 1552943969433540706,
"name": "1 - 3",
"number": 3,
"url": "/stranstviia_emanon/vol1/3",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 1552943969433540707,
"name": "1 - 4",
"number": 4,
"url": "/stranstviia_emanon/vol1/4",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 1552943969433540708,
"name": "1 - 5",
"number": 5,
"url": "/stranstviia_emanon/vol1/5",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 1552943969433541665,
"name": "2 - 1",
"number": 6,
"url": "/stranstviia_emanon/vol2/1",
"scanlator": "Sup!",
"uploadDate": 1415570400000,
"source": "READMANGA_RU"
},
{
"id": 1552943969433541666,
"name": "2 - 2",
"number": 7,
"url": "/stranstviia_emanon/vol2/2",
"scanlator": "Sup!",
"uploadDate": 1419976800000,
"source": "READMANGA_RU"
},
{
"id": 1552943969433541667,
"name": "2 - 3",
"number": 8,
"url": "/stranstviia_emanon/vol2/3",
"scanlator": "Sup!",
"uploadDate": 1427922000000,
"source": "READMANGA_RU"
},
{
"id": 1552943969433541668,
"name": "2 - 4",
"number": 9,
"url": "/stranstviia_emanon/vol2/4",
"scanlator": "Sup!",
"uploadDate": 1436907600000,
"source": "READMANGA_RU"
},
{
"id": 1552943969433541669,
"name": "2 - 5",
"number": 10,
"url": "/stranstviia_emanon/vol2/5",
"scanlator": "Sup!",
"uploadDate": 1446674400000,
"source": "READMANGA_RU"
},
{
"id": 1552943969433541670,
"name": "2 - 6",
"number": 11,
"url": "/stranstviia_emanon/vol2/6",
"scanlator": "Sup!",
"uploadDate": 1451512800000,
"source": "READMANGA_RU"
},
{
"id": 1552943969433542626,
"name": "3 - 1",
"number": 12,
"url": "/stranstviia_emanon/vol3/1",
"scanlator": "Sup!",
"uploadDate": 1461618000000,
"source": "READMANGA_RU"
},
{
"id": 1552943969433542627,
"name": "3 - 2",
"number": 13,
"url": "/stranstviia_emanon/vol3/2",
"scanlator": "Sup!",
"uploadDate": 1461618000000,
"source": "READMANGA_RU"
},
{
"id": 1552943969433542628,
"name": "3 - 3",
"number": 14,
"url": "/stranstviia_emanon/vol3/3",
"scanlator": "",
"uploadDate": 1465851600000,
"source": "READMANGA_RU"
}
],
"source": "READMANGA_RU"
}

View File

@@ -0,0 +1,36 @@
{
"id": -2096681732556647985,
"title": "Странствия Эманон",
"url": "/stranstviia_emanon",
"publicUrl": "https://readmanga.io/stranstviia_emanon",
"rating": 0.9400894,
"isNsfw": true,
"coverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_p.jpg",
"tags": [
{
"title": "Сверхъестественное",
"key": "supernatural",
"source": "READMANGA_RU"
},
{
"title": "Сэйнэн",
"key": "seinen",
"source": "READMANGA_RU"
},
{
"title": "Повседневность",
"key": "slice_of_life",
"source": "READMANGA_RU"
},
{
"title": "Приключения",
"key": "adventure",
"source": "READMANGA_RU"
}
],
"state": "FINISHED",
"largeCoverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_o.jpg",
"description": "Продолжение истории о загадочной девушке по имени Эманон, которая помнит всё, что происходило на Земле за последние три миллиарда лет. \n<br>Начало истории читайте в \"Воспоминаниях Эманон\". \n<div class=\"clearfix\"></div>",
"chapters": [],
"source": "READMANGA_RU"
}

View File

@@ -0,0 +1,136 @@
{
"id": -2096681732556647985,
"title": "Странствия Эманон",
"url": "/stranstviia_emanon",
"publicUrl": "https://readmanga.io/stranstviia_emanon",
"rating": 0.9400894,
"isNsfw": true,
"coverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_p.jpg",
"tags": [
{
"title": "Сверхъестественное",
"key": "supernatural",
"source": "READMANGA_RU"
},
{
"title": "Сэйнэн",
"key": "seinen",
"source": "READMANGA_RU"
},
{
"title": "Повседневность",
"key": "slice_of_life",
"source": "READMANGA_RU"
},
{
"title": "Приключения",
"key": "adventure",
"source": "READMANGA_RU"
}
],
"state": "FINISHED",
"largeCoverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_o.jpg",
"description": "Продолжение истории о загадочной девушке по имени Эманон, которая помнит всё, что происходило на Земле за последние три миллиарда лет. \n<br>Начало истории читайте в \"Воспоминаниях Эманон\". \n<div class=\"clearfix\"></div>",
"chapters": [
{
"id": 3552943969433540704,
"name": "1 - 1",
"number": 1,
"url": "/stranstviia_emanon/vol1/1",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433540705,
"name": "1 - 2",
"number": 2,
"url": "/stranstviia_emanon/vol1/2",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433540706,
"name": "1 - 3",
"number": 3,
"url": "/stranstviia_emanon/vol1/3",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433540707,
"name": "1 - 4",
"number": 4,
"url": "/stranstviia_emanon/vol1/4",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433540708,
"name": "1 - 5",
"number": 5,
"url": "/stranstviia_emanon/vol1/5",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433541665,
"name": "2 - 1",
"number": 6,
"url": "/stranstviia_emanon/vol2/1",
"scanlator": "Sup!",
"uploadDate": 1415570400000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433541666,
"name": "2 - 2",
"number": 7,
"url": "/stranstviia_emanon/vol2/2",
"scanlator": "Sup!",
"uploadDate": 1419976800000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433541667,
"name": "2 - 3",
"number": 8,
"url": "/stranstviia_emanon/vol2/3",
"scanlator": "Sup!",
"uploadDate": 1427922000000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433541668,
"name": "2 - 4",
"number": 9,
"url": "/stranstviia_emanon/vol2/4",
"scanlator": "Sup!",
"uploadDate": 1436907600000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433541669,
"name": "2 - 5",
"number": 10,
"url": "/stranstviia_emanon/vol2/5",
"scanlator": "Sup!",
"uploadDate": 1446674400000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433541670,
"name": "2 - 6",
"number": 11,
"url": "/stranstviia_emanon/vol2/6",
"scanlator": "Sup!",
"uploadDate": 1451512800000,
"source": "READMANGA_RU"
}
],
"source": "READMANGA_RU"
}

View File

@@ -0,0 +1,163 @@
{
"id": -2096681732556647985,
"title": "Странствия Эманон",
"url": "/stranstviia_emanon",
"publicUrl": "https://readmanga.io/stranstviia_emanon",
"rating": 0.9400894,
"isNsfw": true,
"coverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_p.jpg",
"tags": [
{
"title": "Сверхъестественное",
"key": "supernatural",
"source": "READMANGA_RU"
},
{
"title": "Сэйнэн",
"key": "seinen",
"source": "READMANGA_RU"
},
{
"title": "Повседневность",
"key": "slice_of_life",
"source": "READMANGA_RU"
},
{
"title": "Приключения",
"key": "adventure",
"source": "READMANGA_RU"
}
],
"state": "FINISHED",
"largeCoverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_o.jpg",
"description": "Продолжение истории о загадочной девушке по имени Эманон, которая помнит всё, что происходило на Земле за последние три миллиарда лет. \n<br>Начало истории читайте в \"Воспоминаниях Эманон\". \n<div class=\"clearfix\"></div>",
"chapters": [
{
"id": 3552943969433540704,
"name": "1 - 1",
"number": 1,
"url": "/stranstviia_emanon/vol1/1",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433540705,
"name": "1 - 2",
"number": 2,
"url": "/stranstviia_emanon/vol1/2",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433540706,
"name": "1 - 3",
"number": 3,
"url": "/stranstviia_emanon/vol1/3",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433540707,
"name": "1 - 4",
"number": 4,
"url": "/stranstviia_emanon/vol1/4",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433540708,
"name": "1 - 5",
"number": 5,
"url": "/stranstviia_emanon/vol1/5",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433541665,
"name": "2 - 1",
"number": 6,
"url": "/stranstviia_emanon/vol2/1",
"scanlator": "Sup!",
"uploadDate": 1415570400000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433541666,
"name": "2 - 2",
"number": 7,
"url": "/stranstviia_emanon/vol2/2",
"scanlator": "Sup!",
"uploadDate": 1419976800000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433541667,
"name": "2 - 3",
"number": 8,
"url": "/stranstviia_emanon/vol2/3",
"scanlator": "Sup!",
"uploadDate": 1427922000000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433541668,
"name": "2 - 4",
"number": 9,
"url": "/stranstviia_emanon/vol2/4",
"scanlator": "Sup!",
"uploadDate": 1436907600000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433541669,
"name": "2 - 5",
"number": 10,
"url": "/stranstviia_emanon/vol2/5",
"scanlator": "Sup!",
"uploadDate": 1446674400000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433541670,
"name": "2 - 6",
"number": 11,
"url": "/stranstviia_emanon/vol2/6",
"scanlator": "Sup!",
"uploadDate": 1451512800000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433542626,
"name": "3 - 1",
"number": 12,
"url": "/stranstviia_emanon/vol3/1",
"scanlator": "Sup!",
"uploadDate": 1461618000000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433542627,
"name": "3 - 2",
"number": 13,
"url": "/stranstviia_emanon/vol3/2",
"scanlator": "Sup!",
"uploadDate": 1461618000000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433542628,
"name": "3 - 3",
"number": 14,
"url": "/stranstviia_emanon/vol3/3",
"scanlator": "",
"uploadDate": 1465851600000,
"source": "READMANGA_RU"
}
],
"source": "READMANGA_RU"
}

View File

@@ -0,0 +1,154 @@
{
"id": -2096681732556647985,
"title": "Странствия Эманон",
"url": "/stranstviia_emanon",
"publicUrl": "https://readmanga.io/stranstviia_emanon",
"rating": 0.9400894,
"isNsfw": true,
"coverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_p.jpg",
"tags": [
{
"title": "Сверхъестественное",
"key": "supernatural",
"source": "READMANGA_RU"
},
{
"title": "Сэйнэн",
"key": "seinen",
"source": "READMANGA_RU"
},
{
"title": "Повседневность",
"key": "slice_of_life",
"source": "READMANGA_RU"
},
{
"title": "Приключения",
"key": "adventure",
"source": "READMANGA_RU"
}
],
"state": "FINISHED",
"largeCoverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_o.jpg",
"description": "Продолжение истории о загадочной девушке по имени Эманон, которая помнит всё, что происходило на Земле за последние три миллиарда лет. \n<br>Начало истории читайте в \"Воспоминаниях Эманон\". \n<div class=\"clearfix\"></div>",
"chapters": [
{
"id": 3552943969433540704,
"name": "1 - 1",
"number": 1,
"url": "/stranstviia_emanon/vol1/1",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433540705,
"name": "1 - 2",
"number": 2,
"url": "/stranstviia_emanon/vol1/2",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433540706,
"name": "1 - 3",
"number": 3,
"url": "/stranstviia_emanon/vol1/3",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433540707,
"name": "1 - 4",
"number": 4,
"url": "/stranstviia_emanon/vol1/4",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433540708,
"name": "1 - 5",
"number": 5,
"url": "/stranstviia_emanon/vol1/5",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433541666,
"name": "2 - 2",
"number": 7,
"url": "/stranstviia_emanon/vol2/2",
"scanlator": "Sup!",
"uploadDate": 1419976800000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433541667,
"name": "2 - 3",
"number": 8,
"url": "/stranstviia_emanon/vol2/3",
"scanlator": "Sup!",
"uploadDate": 1427922000000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433541668,
"name": "2 - 4",
"number": 9,
"url": "/stranstviia_emanon/vol2/4",
"scanlator": "Sup!",
"uploadDate": 1436907600000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433541669,
"name": "2 - 5",
"number": 10,
"url": "/stranstviia_emanon/vol2/5",
"scanlator": "Sup!",
"uploadDate": 1446674400000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433541670,
"name": "2 - 6",
"number": 11,
"url": "/stranstviia_emanon/vol2/6",
"scanlator": "Sup!",
"uploadDate": 1451512800000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433542626,
"name": "3 - 1",
"number": 12,
"url": "/stranstviia_emanon/vol3/1",
"scanlator": "Sup!",
"uploadDate": 1461618000000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433542627,
"name": "3 - 2",
"number": 13,
"url": "/stranstviia_emanon/vol3/2",
"scanlator": "Sup!",
"uploadDate": 1461618000000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433542628,
"name": "3 - 3",
"number": 14,
"url": "/stranstviia_emanon/vol3/3",
"scanlator": "",
"uploadDate": 1465851600000,
"source": "READMANGA_RU"
}
],
"source": "READMANGA_RU"
}

View File

@@ -1,14 +1,13 @@
package org.koitharu.kotatsu.core.db package org.koitharu.kotatsu.core.db
import androidx.room.testing.MigrationTestHelper import androidx.room.testing.MigrationTestHelper
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import java.io.IOException
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.koitharu.kotatsu.core.db.migrations.* import org.koitharu.kotatsu.core.db.migrations.*
import java.io.IOException
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class MangaDatabaseTest { class MangaDatabaseTest {
@@ -16,8 +15,7 @@ class MangaDatabaseTest {
@get:Rule @get:Rule
val helper: MigrationTestHelper = MigrationTestHelper( val helper: MigrationTestHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(), InstrumentationRegistry.getInstrumentation(),
MangaDatabase::class.java.canonicalName, MangaDatabase::class.java,
FrameworkSQLiteOpenHelperFactory()
) )
@Test @Test
@@ -37,7 +35,6 @@ class MangaDatabaseTest {
} }
} }
private companion object { private companion object {
const val TEST_DB = "test-db" const val TEST_DB = "test-db"
@@ -50,6 +47,9 @@ class MangaDatabaseTest {
Migration5To6(), Migration5To6(),
Migration6To7(), Migration6To7(),
Migration7To8(), Migration7To8(),
Migration8To9(),
Migration9To10(),
Migration10To11(),
) )
} }
} }

View File

@@ -0,0 +1,160 @@
package org.koitharu.kotatsu.tracker.domain
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import kotlinx.coroutines.test.runTest
import okio.buffer
import okio.source
import org.junit.Test
import org.junit.runner.RunWith
import org.koin.test.KoinTest
import org.koin.test.inject
import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga
@RunWith(AndroidJUnit4::class)
class TrackerTest : KoinTest {
private val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
private val mangaAdapter = moshi.adapter(Manga::class.java)
private val historyRegistry by inject<HistoryRepository>()
private val repository by inject<TrackingRepository>()
private val dataRepository by inject<MangaDataRepository>()
private val tracker by inject<Tracker>()
@Test
fun noUpdates() = runTest {
val manga = loadManga("full.json")
tracker.deleteTrack(manga.id)
tracker.checkUpdates(manga, commit = true).apply {
assertFalse(isValid)
assert(newChapters.isEmpty())
}
assertEquals(0, repository.getNewChaptersCount(manga.id))
tracker.checkUpdates(manga, commit = true).apply {
assertTrue(isValid)
assert(newChapters.isEmpty())
}
assertEquals(0, repository.getNewChaptersCount(manga.id))
}
@Test
fun hasUpdates() = runTest {
val mangaFirst = loadManga("first_chapters.json")
val mangaFull = loadManga("full.json")
tracker.deleteTrack(mangaFirst.id)
tracker.checkUpdates(mangaFirst, commit = true).apply {
assertFalse(isValid)
assert(newChapters.isEmpty())
}
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
tracker.checkUpdates(mangaFull, commit = true).apply {
assertTrue(isValid)
assertEquals(3, newChapters.size)
}
assertEquals(3, repository.getNewChaptersCount(mangaFirst.id))
tracker.checkUpdates(mangaFull, commit = true).apply {
assertTrue(isValid)
assert(newChapters.isEmpty())
}
assertEquals(3, repository.getNewChaptersCount(mangaFirst.id))
}
@Test
fun badIds() = runTest {
val mangaFirst = loadManga("first_chapters.json")
val mangaBad = loadManga("bad_ids.json")
tracker.deleteTrack(mangaFirst.id)
tracker.checkUpdates(mangaFirst, commit = true).apply {
assertFalse(isValid)
assert(newChapters.isEmpty())
}
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
tracker.checkUpdates(mangaBad, commit = true).apply {
assertFalse(isValid)
assert(newChapters.isEmpty())
}
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
tracker.checkUpdates(mangaFirst, commit = true).apply {
assertFalse(isValid)
assert(newChapters.isEmpty())
}
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
}
@Test
fun badIds2() = runTest {
val mangaFirst = loadManga("first_chapters.json")
val mangaBad = loadManga("bad_ids.json")
val mangaFull = loadManga("full.json")
tracker.deleteTrack(mangaFirst.id)
tracker.checkUpdates(mangaFirst, commit = true).apply {
assertFalse(isValid)
assert(newChapters.isEmpty())
}
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
tracker.checkUpdates(mangaFull, commit = true).apply {
assertTrue(isValid)
assertEquals(3, newChapters.size)
}
assertEquals(3, repository.getNewChaptersCount(mangaFull.id))
tracker.checkUpdates(mangaBad, commit = true).apply {
assertFalse(isValid)
assert(newChapters.isEmpty())
}
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
}
@Test
fun fullReset() = runTest {
val mangaFull = loadManga("full.json")
val mangaFirst = loadManga("first_chapters.json")
val mangaEmpty = loadManga("empty.json")
tracker.deleteTrack(mangaFull.id)
assertEquals(0, repository.getNewChaptersCount(mangaFull.id))
tracker.checkUpdates(mangaFull, commit = true).apply {
assertFalse(isValid)
assert(newChapters.isEmpty())
}
assertEquals(0, repository.getNewChaptersCount(mangaFull.id))
tracker.checkUpdates(mangaEmpty, commit = true).apply {
assert(newChapters.isEmpty())
}
assertEquals(0, repository.getNewChaptersCount(mangaFull.id))
tracker.checkUpdates(mangaFirst, commit = true).apply {
assertFalse(isValid)
assert(newChapters.isEmpty())
}
assertEquals(0, repository.getNewChaptersCount(mangaFull.id))
tracker.checkUpdates(mangaFull, commit = true).apply {
assertTrue(isValid)
assertEquals(3, newChapters.size)
}
assertEquals(3, repository.getNewChaptersCount(mangaFull.id))
tracker.checkUpdates(mangaEmpty, commit = true).apply {
assertFalse(isValid)
assert(newChapters.isEmpty())
}
assertEquals(0, repository.getNewChaptersCount(mangaFull.id))
}
private suspend fun loadManga(name: String): Manga {
val assets = InstrumentationRegistry.getInstrumentation().context.assets
val manga = assets.open("manga/$name").use {
mangaAdapter.fromJson(it.source().buffer())
} ?: throw RuntimeException("Cannot read manga from json \"$name\"")
dataRepository.storeManga(manga)
return manga
}
}

View File

@@ -17,6 +17,9 @@ import org.koitharu.kotatsu.history.data.HistoryDao
import org.koitharu.kotatsu.history.data.HistoryEntity import org.koitharu.kotatsu.history.data.HistoryEntity
import org.koitharu.kotatsu.suggestions.data.SuggestionDao import org.koitharu.kotatsu.suggestions.data.SuggestionDao
import org.koitharu.kotatsu.suggestions.data.SuggestionEntity import org.koitharu.kotatsu.suggestions.data.SuggestionEntity
import org.koitharu.kotatsu.tracker.data.TrackEntity
import org.koitharu.kotatsu.tracker.data.TrackLogEntity
import org.koitharu.kotatsu.tracker.data.TracksDao
@Database( @Database(
entities = [ entities = [

View File

@@ -1,8 +1,8 @@
package org.koitharu.kotatsu.core.db.dao package org.koitharu.kotatsu.core.db.dao
import androidx.room.* import androidx.room.*
import org.koitharu.kotatsu.core.db.entity.TrackLogEntity import org.koitharu.kotatsu.tracker.data.TrackLogEntity
import org.koitharu.kotatsu.core.db.entity.TrackLogWithManga import org.koitharu.kotatsu.tracker.data.TrackLogWithManga
@Dao @Dao
interface TrackLogsDao { interface TrackLogsDao {

View File

@@ -1,7 +1,5 @@
package org.koitharu.kotatsu.core.db.entity package org.koitharu.kotatsu.core.db.entity
import java.util.*
import org.koitharu.kotatsu.core.model.TrackingLogItem
import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.parsers.util.toTitleCase import org.koitharu.kotatsu.parsers.util.toTitleCase
@@ -35,13 +33,6 @@ fun MangaEntity.toManga(tags: Set<MangaTag>) = Manga(
fun MangaWithTags.toManga() = manga.toManga(tags.toMangaTags()) fun MangaWithTags.toManga() = manga.toManga(tags.toMangaTags())
fun TrackLogWithManga.toTrackingLogItem() = TrackingLogItem(
id = trackLog.id,
chapters = trackLog.chapters.split('\n').filterNot { x -> x.isEmpty() },
manga = manga.toManga(tags.toMangaTags()),
createdAt = Date(trackLog.createdAt)
)
// Model to entity // Model to entity
fun Manga.toEntity() = MangaEntity( fun Manga.toEntity() = MangaEntity(

View File

@@ -77,7 +77,7 @@ class HistoryRepository(
scroll = scroll.toFloat(), // we migrate to int, but decide to not update database scroll = scroll.toFloat(), // we migrate to int, but decide to not update database
) )
) )
trackingRepository.upsert(manga) trackingRepository.syncWithHistory(manga, chapterId)
} }
} }

View File

@@ -43,7 +43,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
} }
findPreference<Preference>(AppSettings.KEY_UPDATES_FEED_CLEAR)?.let { pref -> findPreference<Preference>(AppSettings.KEY_UPDATES_FEED_CLEAR)?.let { pref ->
viewLifecycleScope.launchWhenResumed { viewLifecycleScope.launchWhenResumed {
val items = trackerRepo.count() val items = trackerRepo.getLogsCount()
pref.summary = pref.summary =
pref.context.resources.getQuantityString(R.plurals.items, items, items) pref.context.resources.getQuantityString(R.plurals.items, items, items)
} }
@@ -142,4 +142,4 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
} }
}.show() }.show()
} }
} }

View File

@@ -14,7 +14,7 @@ val trackerModule
factory { TrackingRepository(get()) } factory { TrackingRepository(get()) }
factory { TrackerNotificationChannels(androidContext(), get()) } factory { TrackerNotificationChannels(androidContext(), get()) }
factory { Tracker(get()) } factory { Tracker(get(), get(), get()) }
viewModel { FeedViewModel(get()) } viewModel { FeedViewModel(get()) }
} }

View File

@@ -0,0 +1,13 @@
package org.koitharu.kotatsu.tracker.data
import java.util.*
import org.koitharu.kotatsu.core.db.entity.toManga
import org.koitharu.kotatsu.core.db.entity.toMangaTags
import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem
fun TrackLogWithManga.toTrackingLogItem() = TrackingLogItem(
id = trackLog.id,
chapters = trackLog.chapters.split('\n').filterNot { x -> x.isEmpty() },
manga = manga.toManga(tags.toMangaTags()),
createdAt = Date(trackLog.createdAt)
)

View File

@@ -1,9 +1,10 @@
package org.koitharu.kotatsu.core.db.entity package org.koitharu.kotatsu.tracker.data
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey import androidx.room.ForeignKey
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import org.koitharu.kotatsu.core.db.entity.MangaEntity
@Entity( @Entity(
tableName = "tracks", tableName = "tracks",
@@ -19,9 +20,11 @@ import androidx.room.PrimaryKey
class TrackEntity( class TrackEntity(
@PrimaryKey(autoGenerate = false) @PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "manga_id") val mangaId: Long, @ColumnInfo(name = "manga_id") val mangaId: Long,
@get:Deprecated(message = "Should not be used", level = DeprecationLevel.ERROR)
@ColumnInfo(name = "chapters_total") val totalChapters: Int, @ColumnInfo(name = "chapters_total") val totalChapters: Int,
@ColumnInfo(name = "last_chapter_id") val lastChapterId: Long, @ColumnInfo(name = "last_chapter_id") val lastChapterId: Long,
@ColumnInfo(name = "chapters_new") val newChapters: Int, @ColumnInfo(name = "chapters_new") val newChapters: Int,
@ColumnInfo(name = "last_check") val lastCheck: Long, @ColumnInfo(name = "last_check") val lastCheck: Long,
@get:Deprecated(message = "Should not be used", level = DeprecationLevel.ERROR)
@ColumnInfo(name = "last_notified_id") val lastNotifiedChapterId: Long @ColumnInfo(name = "last_notified_id") val lastNotifiedChapterId: Long
) )

View File

@@ -1,9 +1,10 @@
package org.koitharu.kotatsu.core.db.entity package org.koitharu.kotatsu.tracker.data
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey import androidx.room.ForeignKey
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import org.koitharu.kotatsu.core.db.entity.MangaEntity
@Entity( @Entity(
tableName = "track_logs", tableName = "track_logs",

View File

@@ -1,8 +1,11 @@
package org.koitharu.kotatsu.core.db.entity package org.koitharu.kotatsu.tracker.data
import androidx.room.Embedded import androidx.room.Embedded
import androidx.room.Junction import androidx.room.Junction
import androidx.room.Relation import androidx.room.Relation
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.MangaTagsEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
class TrackLogWithManga( class TrackLogWithManga(
@Embedded val trackLog: TrackLogEntity, @Embedded val trackLog: TrackLogEntity,

View File

@@ -1,7 +1,6 @@
package org.koitharu.kotatsu.core.db.dao package org.koitharu.kotatsu.tracker.data
import androidx.room.* import androidx.room.*
import org.koitharu.kotatsu.core.db.entity.TrackEntity
@Dao @Dao
abstract class TracksDao { abstract class TracksDao {

View File

@@ -1,132 +1,115 @@
package org.koitharu.kotatsu.tracker.domain package org.koitharu.kotatsu.tracker.domain
import org.koitharu.kotatsu.core.model.MangaTracking import androidx.annotation.VisibleForTesting
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.tracker.domain.model.MangaTracking
import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates
import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels
import org.koitharu.kotatsu.tracker.work.TrackingItem
class Tracker( class Tracker(
private val settings: AppSettings,
private val repository: TrackingRepository, private val repository: TrackingRepository,
private val channels: TrackerNotificationChannels,
) { ) {
suspend fun fetchUpdates(track: MangaTracking, commit: Boolean): MangaUpdates { suspend fun getAllTracks(): List<TrackingItem> {
val repo = MangaRepository(track.manga.source) val sources = settings.trackSources
val details = repo.getDetails(track.manga) if (sources.isEmpty()) {
val chapters = details.chapters.orEmpty() return emptyList()
if (track.isEmpty()) {
// first check or manga was empty on last check
if (commit) {
repository.storeTrackResult(
mangaId = track.manga.id,
knownChaptersCount = chapters.size,
lastChapterId = chapters.lastOrNull()?.id ?: 0L,
previousTrackChapterId = 0L,
newChapters = emptyList(),
saveTrackLog = false,
)
}
return MangaUpdates(
manga = details,
newChapters = emptyList(),
)
} }
val newChapters = details.getNewChapters(track.lastChapterId) val knownIds = HashSet<Manga>()
if (newChapters.isEmpty()) { val result = ArrayList<TrackingItem>()
if (commit) { // Favourites
repository.storeTrackResult( if (AppSettings.TRACK_FAVOURITES in sources) {
mangaId = track.manga.id, val favourites = repository.getAllFavouritesManga()
knownChaptersCount = chapters.size, channels.updateChannels(favourites.keys)
lastChapterId = chapters.lastOrNull()?.id ?: 0L, for ((category, mangaList) in favourites) {
previousTrackChapterId = 0L, if (!category.isTrackingEnabled || mangaList.isEmpty()) {
newChapters = emptyList(), continue
saveTrackLog = false, }
) val categoryTracks = repository.getTracks(mangaList)
} val channelId = if (channels.isFavouriteNotificationsEnabled(category)) {
return MangaUpdates( channels.getFavouritesChannelId(category.id)
manga = details,
newChapters = emptyList(),
)
}
return when {
// the same chapters count
chapters.size == track.knownChaptersCount -> {
if (chapters.lastOrNull()?.id == track.lastChapterId) {
// manga was not updated. skip
MangaUpdates(
manga = details,
newChapters = emptyList(),
)
} else { } else {
// number of chapters still the same, bu last chapter changed. null
// maybe some chapters are removed. we need to find last known chapter }
val knownChapter = chapters.indexOfLast { it.id == track.lastChapterId } for (track in categoryTracks) {
if (knownChapter == -1) { if (knownIds.add(track.manga)) {
// confuse. reset anything result.add(TrackingItem(track, channelId))
if (commit) {
repository.storeTrackResult(
mangaId = track.manga.id,
knownChaptersCount = chapters.size,
lastChapterId = chapters.lastOrNull()?.id ?: 0L,
previousTrackChapterId = 0L,
newChapters = emptyList(),
saveTrackLog = false,
)
}
MangaUpdates(
manga = details,
newChapters = emptyList(),
)
} else {
val newChapters = chapters.takeLast(chapters.size - knownChapter + 1)
if (commit) {
repository.storeTrackResult(
mangaId = track.manga.id,
knownChaptersCount = knownChapter + 1,
lastChapterId = track.lastChapterId,
previousTrackChapterId = track.lastNotifiedChapterId,
newChapters = newChapters,
saveTrackLog = true,
)
}
MangaUpdates(
manga = details,
newChapters = details.getNewChapters(track.lastNotifiedChapterId),
)
} }
} }
} }
else -> { }
val newChapters = chapters.takeLast(chapters.size - track.knownChaptersCount) // History
if (commit) { if (AppSettings.TRACK_HISTORY in sources) {
repository.storeTrackResult( val history = repository.getAllHistoryManga()
mangaId = track.manga.id, val historyTracks = repository.getTracks(history)
knownChaptersCount = track.knownChaptersCount, val channelId = if (channels.isHistoryNotificationsEnabled()) {
lastChapterId = track.lastChapterId, channels.getHistoryChannelId()
previousTrackChapterId = track.lastNotifiedChapterId, } else {
newChapters = newChapters, null
saveTrackLog = true, }
) for (track in historyTracks) {
if (knownIds.add(track.manga)) {
result.add(TrackingItem(track, channelId))
} }
MangaUpdates( }
manga = details, }
newChapters = details.getNewChapters(track.lastNotifiedChapterId), result.trimToSize()
) return result
}
suspend fun gc() {
repository.gc()
}
suspend fun fetchUpdates(track: MangaTracking, commit: Boolean): MangaUpdates {
val manga = MangaRepository(track.manga.source).getDetails(track.manga)
val updates = compare(track, manga)
if (commit) {
repository.saveUpdates(updates)
}
return updates
}
@VisibleForTesting
suspend fun checkUpdates(manga: Manga, commit: Boolean): MangaUpdates {
val track = repository.getTrack(manga)
val updates = compare(track, manga)
if (commit) {
repository.saveUpdates(updates)
}
return updates
}
@VisibleForTesting
suspend fun deleteTrack(mangaId: Long) {
repository.deleteTrack(mangaId)
}
/**
* The main functionality of tracker: check new chapters in [manga] comparing to the [track]
*/
private fun compare(track: MangaTracking, manga: Manga): MangaUpdates {
if (track.isEmpty()) {
// first check or manga was empty on last check
return MangaUpdates(manga, emptyList(), isValid = false)
}
val chapters = requireNotNull(manga.chapters)
val newChapters = chapters.takeLastWhile { x -> x.id != track.lastChapterId }
return when {
newChapters.isEmpty() -> {
return MangaUpdates(manga, emptyList(), isValid = chapters.lastOrNull()?.id == track.lastChapterId)
}
newChapters.size == chapters.size -> {
return MangaUpdates(manga, emptyList(), isValid = false)
}
else -> {
return MangaUpdates(manga, newChapters, isValid = true)
} }
} }
} }
private fun Manga.getNewChapters(lastChapterId: Long): List<MangaChapter> {
val chapters = chapters ?: return emptyList()
if (lastChapterId == 0L) {
return emptyList()
}
val raw = chapters.takeLastWhile { x -> x.id != lastChapterId }
return if (raw.isEmpty() || raw.size == chapters.size) {
emptyList()
} else {
raw
}
}
} }

View File

@@ -1,17 +1,24 @@
package org.koitharu.kotatsu.tracker.domain package org.koitharu.kotatsu.tracker.domain
import androidx.annotation.VisibleForTesting
import androidx.room.withTransaction import androidx.room.withTransaction
import java.util.* import java.util.*
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.* import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.toManga
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.MangaTracking
import org.koitharu.kotatsu.core.model.TrackingLogItem
import org.koitharu.kotatsu.favourites.data.toFavouriteCategory import org.koitharu.kotatsu.favourites.data.toFavouriteCategory
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.tracker.data.TrackEntity
import org.koitharu.kotatsu.tracker.data.TrackLogEntity
import org.koitharu.kotatsu.tracker.data.toTrackingLogItem
import org.koitharu.kotatsu.tracker.domain.model.MangaTracking
import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates
import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem
private const val NO_ID = 0L
class TrackingRepository( class TrackingRepository(
private val db: MangaDatabase, private val db: MangaDatabase,
@@ -21,42 +28,38 @@ class TrackingRepository(
return db.tracksDao.findNewChapters(mangaId) ?: 0 return db.tracksDao.findNewChapters(mangaId) ?: 0
} }
suspend fun getHistoryManga(): List<Manga> {
return db.historyDao.findAllManga().toMangaList()
}
suspend fun getFavouritesManga(): Map<FavouriteCategory, List<Manga>> {
val categories = db.favouriteCategoriesDao.findAll()
return categories.associateTo(LinkedHashMap(categories.size)) { categoryEntity ->
categoryEntity.toFavouriteCategory() to db.favouritesDao.findAllManga(categoryEntity.categoryId)
.toMangaList()
}
}
suspend fun getCategoriesCount(): IntArray {
val categories = db.favouriteCategoriesDao.findAll()
return intArrayOf(
categories.count { it.track },
categories.size,
)
}
suspend fun getTracks(mangaList: Collection<Manga>): List<MangaTracking> { suspend fun getTracks(mangaList: Collection<Manga>): List<MangaTracking> {
val ids = mangaList.mapToSet { it.id } val ids = mangaList.mapToSet { it.id }
val tracks = db.tracksDao.findAll(ids).groupBy { it.mangaId } val tracks = db.tracksDao.findAll(ids).groupBy { it.mangaId }
return mangaList // TODO optimize val idSet = HashSet<Long>()
.filterNot { it.source == MangaSource.LOCAL } val result = ArrayList<MangaTracking>(mangaList.size)
.distinctBy { it.id } for (item in mangaList) {
.map { manga -> if (item.source == MangaSource.LOCAL || !idSet.add(item.id)) {
val track = tracks[manga.id]?.singleOrNull() continue
MangaTracking(
manga = manga,
knownChaptersCount = track?.totalChapters ?: -1,
lastChapterId = track?.lastChapterId ?: 0L,
lastNotifiedChapterId = track?.lastNotifiedChapterId ?: 0L,
lastCheck = track?.lastCheck?.takeUnless { it == 0L }?.let(::Date)
)
} }
val track = tracks[item.id]?.lastOrNull()
result += MangaTracking(
manga = item,
lastChapterId = track?.lastChapterId ?: NO_ID,
lastCheck = track?.lastCheck?.takeUnless { it == 0L }?.let(::Date)
)
}
return result
}
@VisibleForTesting
suspend fun getTrack(manga: Manga): MangaTracking {
val track = db.tracksDao.find(manga.id)
return MangaTracking(
manga = manga,
lastChapterId = track?.lastChapterId ?: NO_ID,
lastCheck = track?.lastCheck?.takeUnless { it == 0L }?.let(::Date)
)
}
@VisibleForTesting
suspend fun deleteTrack(mangaId: Long) {
db.tracksDao.delete(mangaId)
} }
suspend fun getTrackingLog(offset: Int, limit: Int): List<TrackingLogItem> { suspend fun getTrackingLog(offset: Int, limit: Int): List<TrackingLogItem> {
@@ -65,7 +68,7 @@ class TrackingRepository(
} }
} }
suspend fun count() = db.trackLogsDao.count() suspend fun getLogsCount() = db.trackLogsDao.count()
suspend fun clearLogs() = db.trackLogsDao.clear() suspend fun clearLogs() = db.trackLogsDao.clear()
@@ -76,50 +79,85 @@ class TrackingRepository(
} }
} }
suspend fun storeTrackResult( suspend fun saveUpdates(updates: MangaUpdates) {
mangaId: Long,
knownChaptersCount: Int, // how many chapters user already seen
lastChapterId: Long, // in upstream manga
newChapters: List<MangaChapter>,
previousTrackChapterId: Long, // from previous check
saveTrackLog: Boolean,
) {
db.withTransaction { db.withTransaction {
val entity = TrackEntity( val track = getOrCreateTrack(updates.manga.id).mergeWith(updates)
mangaId = mangaId, db.tracksDao.upsert(track)
newChapters = newChapters.size, if (updates.isValid && updates.newChapters.isNotEmpty()) {
lastCheck = System.currentTimeMillis(), val logEntity = TrackLogEntity(
lastChapterId = lastChapterId, mangaId = updates.manga.id,
totalChapters = knownChaptersCount, chapters = updates.newChapters.joinToString("\n") { x -> x.name },
lastNotifiedChapterId = newChapters.lastOrNull()?.id ?: previousTrackChapterId createdAt = System.currentTimeMillis(),
) )
db.tracksDao.upsert(entity) db.trackLogsDao.insert(logEntity)
if (saveTrackLog && previousTrackChapterId != 0L) {
val foundChapters = newChapters.takeLastWhile { x -> x.id != previousTrackChapterId }
if (foundChapters.isNotEmpty()) {
val logEntity = TrackLogEntity(
mangaId = mangaId,
chapters = foundChapters.joinToString("\n") { x -> x.name },
createdAt = System.currentTimeMillis()
)
db.trackLogsDao.insert(logEntity)
}
} }
} }
} }
suspend fun upsert(manga: Manga) { suspend fun syncWithHistory(manga: Manga, chapterId: Long) {
val chapters = manga.chapters ?: return val chapters = manga.chapters ?: return
val chapterIndex = chapters.indexOfFirst { x -> x.id == chapterId }
val track = getOrCreateTrack(manga.id)
val lastNewChapterIndex = chapters.size - track.newChapters
val lastChapterId = chapters.lastOrNull()?.id ?: NO_ID
val entity = TrackEntity( val entity = TrackEntity(
mangaId = manga.id, mangaId = manga.id,
totalChapters = chapters.size, totalChapters = chapters.size,
lastChapterId = chapters.lastOrNull()?.id ?: 0L, lastChapterId = lastChapterId,
newChapters = 0, newChapters = when {
track.newChapters == 0 -> 0
chapterIndex < 0 -> track.newChapters
chapterIndex > lastNewChapterIndex -> chapters.lastIndex - chapterIndex
else -> track.newChapters
},
lastCheck = System.currentTimeMillis(), lastCheck = System.currentTimeMillis(),
lastNotifiedChapterId = 0L lastNotifiedChapterId = lastChapterId,
) )
db.tracksDao.upsert(entity) db.tracksDao.upsert(entity)
} }
suspend fun getCategoriesCount(): IntArray {
val categories = db.favouriteCategoriesDao.findAll()
return intArrayOf(
categories.count { it.track },
categories.size,
)
}
suspend fun getAllFavouritesManga(): Map<FavouriteCategory, List<Manga>> {
val categories = db.favouriteCategoriesDao.findAll()
return categories.associateTo(LinkedHashMap(categories.size)) { categoryEntity ->
categoryEntity.toFavouriteCategory() to
db.favouritesDao.findAllManga(categoryEntity.categoryId).toMangaList()
}
}
suspend fun getAllHistoryManga(): List<Manga> {
return db.historyDao.findAllManga().toMangaList()
}
private suspend fun getOrCreateTrack(mangaId: Long): TrackEntity {
return db.tracksDao.find(mangaId) ?: TrackEntity(
mangaId = mangaId,
totalChapters = 0,
lastChapterId = 0L,
newChapters = 0,
lastCheck = 0L,
lastNotifiedChapterId = 0L,
)
}
private fun TrackEntity.mergeWith(updates: MangaUpdates): TrackEntity {
val chapters = updates.manga.chapters.orEmpty()
return TrackEntity(
mangaId = mangaId,
totalChapters = chapters.size,
lastChapterId = chapters.lastOrNull()?.id ?: NO_ID,
newChapters = if (updates.isValid) newChapters + updates.newChapters.size else 0,
lastCheck = System.currentTimeMillis(),
lastNotifiedChapterId = NO_ID,
)
}
private fun Collection<MangaEntity>.toMangaList() = map { it.toManga(emptySet()) } private fun Collection<MangaEntity>.toMangaList() = map { it.toManga(emptySet()) }
} }

View File

@@ -1,18 +1,16 @@
package org.koitharu.kotatsu.core.model package org.koitharu.kotatsu.tracker.domain.model
import java.util.* import java.util.*
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
class MangaTracking( class MangaTracking(
val manga: Manga, val manga: Manga,
val knownChaptersCount: Int,
val lastChapterId: Long, val lastChapterId: Long,
val lastNotifiedChapterId: Long,
val lastCheck: Date?, val lastCheck: Date?,
) { ) {
fun isEmpty(): Boolean { fun isEmpty(): Boolean {
return knownChaptersCount <= 0 || lastChapterId == 0L return lastChapterId == 0L
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
@@ -22,9 +20,7 @@ class MangaTracking(
other as MangaTracking other as MangaTracking
if (manga != other.manga) return false if (manga != other.manga) return false
if (knownChaptersCount != other.knownChaptersCount) return false
if (lastChapterId != other.lastChapterId) return false if (lastChapterId != other.lastChapterId) return false
if (lastNotifiedChapterId != other.lastNotifiedChapterId) return false
if (lastCheck != other.lastCheck) return false if (lastCheck != other.lastCheck) return false
return true return true
@@ -32,9 +28,7 @@ class MangaTracking(
override fun hashCode(): Int { override fun hashCode(): Int {
var result = manga.hashCode() var result = manga.hashCode()
result = 31 * result + knownChaptersCount
result = 31 * result + lastChapterId.hashCode() result = 31 * result + lastChapterId.hashCode()
result = 31 * result + lastNotifiedChapterId.hashCode()
result = 31 * result + (lastCheck?.hashCode() ?: 0) result = 31 * result + (lastCheck?.hashCode() ?: 0)
return result return result
} }

View File

@@ -6,4 +6,5 @@ import org.koitharu.kotatsu.parsers.model.MangaChapter
class MangaUpdates( class MangaUpdates(
val manga: Manga, val manga: Manga,
val newChapters: List<MangaChapter>, val newChapters: List<MangaChapter>,
val isValid: Boolean,
) )

View File

@@ -1,9 +1,7 @@
package org.koitharu.kotatsu.core.model package org.koitharu.kotatsu.tracker.domain.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import org.koitharu.kotatsu.parsers.model.Manga
import java.util.* import java.util.*
import org.koitharu.kotatsu.parsers.model.Manga
data class TrackingLogItem( data class TrackingLogItem(
val id: Long, val id: Long,

View File

@@ -1,6 +1,8 @@
package org.koitharu.kotatsu.tracker.ui package org.koitharu.kotatsu.tracker.ui
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import java.util.*
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.cancelAndJoin
@@ -9,16 +11,14 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.model.TrackingLogItem
import org.koitharu.kotatsu.core.ui.DateTimeAgo import org.koitharu.kotatsu.core.ui.DateTimeAgo
import org.koitharu.kotatsu.list.ui.model.* import org.koitharu.kotatsu.list.ui.model.*
import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem
import org.koitharu.kotatsu.tracker.ui.model.toFeedItem import org.koitharu.kotatsu.tracker.ui.model.toFeedItem
import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.daysDiff import org.koitharu.kotatsu.utils.ext.daysDiff
import java.util.*
import java.util.concurrent.TimeUnit
class FeedViewModel( class FeedViewModel(
private val repository: TrackingRepository private val repository: TrackingRepository

View File

@@ -1,6 +1,6 @@
package org.koitharu.kotatsu.tracker.ui.model package org.koitharu.kotatsu.tracker.ui.model
import org.koitharu.kotatsu.core.model.TrackingLogItem import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem
fun TrackingLogItem.toFeedItem() = FeedItem( fun TrackingLogItem.toFeedItem() = FeedItem(
id = id, id = id,

View File

@@ -25,7 +25,6 @@ import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.tracker.domain.Tracker import org.koitharu.kotatsu.tracker.domain.Tracker
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.PendingIntentCompat import org.koitharu.kotatsu.utils.PendingIntentCompat
import org.koitharu.kotatsu.utils.ext.referer import org.koitharu.kotatsu.utils.ext.referer
import org.koitharu.kotatsu.utils.ext.toBitmapOrNull import org.koitharu.kotatsu.utils.ext.toBitmapOrNull
@@ -41,10 +40,7 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
} }
private val coil by inject<ImageLoader>() private val coil by inject<ImageLoader>()
private val repository by inject<TrackingRepository>()
private val settings by inject<AppSettings>() private val settings by inject<AppSettings>()
private val channels by inject<TrackerNotificationChannels>()
private val tracker by inject<Tracker>() private val tracker by inject<Tracker>()
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
@@ -54,7 +50,7 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
if (TAG in tags) { // not expedited if (TAG in tags) { // not expedited
trySetForeground() trySetForeground()
} }
val tracks = getAllTracks() val tracks = tracker.getAllTracks()
var success = 0 var success = 0
val workData = Data.Builder().putInt(DATA_TOTAL, tracks.size) val workData = Data.Builder().putInt(DATA_TOTAL, tracks.size)
@@ -75,7 +71,7 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
) )
} }
} }
repository.gc() tracker.gc()
return if (success == 0) { return if (success == 0) {
Result.retry() Result.retry()
} else { } else {
@@ -83,53 +79,6 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
} }
} }
private suspend fun getAllTracks(): List<TrackingItem> {
val sources = settings.trackSources
if (sources.isEmpty()) {
return emptyList()
}
val knownIds = HashSet<Manga>()
val result = ArrayList<TrackingItem>()
// Favourites
if (AppSettings.TRACK_FAVOURITES in sources) {
val favourites = repository.getFavouritesManga()
channels.updateChannels(favourites.keys)
for ((category, mangaList) in favourites) {
if (!category.isTrackingEnabled || mangaList.isEmpty()) {
continue
}
val categoryTracks = repository.getTracks(mangaList)
val channelId = if (channels.isFavouriteNotificationsEnabled(category)) {
channels.getFavouritesChannelId(category.id)
} else {
null
}
for (track in categoryTracks) {
if (knownIds.add(track.manga)) {
result.add(TrackingItem(track, channelId))
}
}
}
}
// History
if (AppSettings.TRACK_HISTORY in sources) {
val history = repository.getHistoryManga()
val historyTracks = repository.getTracks(history)
val channelId = if (channels.isHistoryNotificationsEnabled()) {
channels.getHistoryChannelId()
} else {
null
}
for (track in historyTracks) {
if (knownIds.add(track.manga)) {
result.add(TrackingItem(track, channelId))
}
}
}
result.trimToSize()
return result
}
private suspend fun showNotification(manga: Manga, channelId: String?, newChapters: List<MangaChapter>) { private suspend fun showNotification(manga: Manga, channelId: String?, newChapters: List<MangaChapter>) {
if (newChapters.isEmpty() || channelId == null) { if (newChapters.isEmpty() || channelId == null) {
return return

View File

@@ -1,6 +1,6 @@
package org.koitharu.kotatsu.tracker.work package org.koitharu.kotatsu.tracker.work
import org.koitharu.kotatsu.core.model.MangaTracking import org.koitharu.kotatsu.tracker.domain.model.MangaTracking
class TrackingItem( class TrackingItem(
val tracking: MangaTracking, val tracking: MangaTracking,

View File

@@ -1,32 +0,0 @@
package org.koitharu.kotatsu.utils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.rules.TestWatcher
import org.junit.runner.Description
class CoroutineTestRule(
private val testDispatcher: TestDispatcher = StandardTestDispatcher(),
) : TestWatcher() {
override fun starting(description: Description) {
super.starting(description)
Dispatchers.setMain(testDispatcher)
}
override fun finished(description: Description) {
super.finished(description)
Dispatchers.resetMain()
}
fun runBlockingTest(block: suspend CoroutineScope.() -> Unit) {
runBlocking(testDispatcher) {
block()
}
}
}