Refactor tracker and add tests
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
}
|
}
|
||||||
163
app/src/androidTest/assets/manga/bad_ids.json
Normal file
163
app/src/androidTest/assets/manga/bad_ids.json
Normal 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"
|
||||||
|
}
|
||||||
36
app/src/androidTest/assets/manga/empty.json
Normal file
36
app/src/androidTest/assets/manga/empty.json
Normal 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"
|
||||||
|
}
|
||||||
136
app/src/androidTest/assets/manga/first_chapters.json
Normal file
136
app/src/androidTest/assets/manga/first_chapters.json
Normal 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"
|
||||||
|
}
|
||||||
163
app/src/androidTest/assets/manga/full.json
Normal file
163
app/src/androidTest/assets/manga/full.json
Normal 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"
|
||||||
|
}
|
||||||
154
app/src/androidTest/assets/manga/without_middle_chapter.json
Normal file
154
app/src/androidTest/assets/manga/without_middle_chapter.json
Normal 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"
|
||||||
|
}
|
||||||
@@ -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(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 = [
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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()) }
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
)
|
||||||
@@ -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
|
||||||
)
|
)
|
||||||
@@ -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",
|
||||||
@@ -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,
|
||||||
@@ -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 {
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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()) }
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
)
|
)
|
||||||
@@ -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,
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user