diff --git a/package.json b/package.json
index fc660e5..acfdf48 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
"@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.1",
"bootstrap-vue-next": "^0.13.3",
+ "idb": "^7.1.1",
"pinia": "^2.1.6",
"pinia-class-component": "^0.9.4",
"vue": "^3.3.4",
diff --git a/src/components/AniListUserSearch.vue b/src/components/AniListUserSearch.vue
index 1e68a04..50aefe0 100644
--- a/src/components/AniListUserSearch.vue
+++ b/src/components/AniListUserSearch.vue
@@ -1,30 +1,44 @@
-
{{ listName }}
-
+ {{ localeListName }}
+
+
+
+
{{ cd(data).item.media?.title.userPreferred }}
+
{{ cd(data).item.media?.title.english }}
+
+
+
+
+
+ {{ cd(data).value }}
+
- {{ data.value + ' / ' + (data.item.media.chapters ?? '?') }}
+ {{ cd(data).value + ' / ' + (cd(data).item.media?.chapters ?? '?') }}
+
+
+
+ {{ c.chapter + ' (' + c.group + ')' }}
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -96,4 +206,8 @@ export default class MangaList extends Vue {
.manga-table tr th:last-child {
padding-inline-end: 1rem;
}
+
+.manga-table > :not(caption) tr:last-child td {
+ border-bottom-width: 0;
+}
diff --git a/src/components/MangaLists.vue b/src/components/MangaLists.vue
index 2063149..0c81efe 100644
--- a/src/components/MangaLists.vue
+++ b/src/components/MangaLists.vue
@@ -1,25 +1,68 @@
-
+
diff --git a/src/components/MangaUpdatesUpdater.vue b/src/components/MangaUpdatesUpdater.vue
new file mode 100644
index 0000000..dc78e2b
--- /dev/null
+++ b/src/components/MangaUpdatesUpdater.vue
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
diff --git a/src/components/NavBar.vue b/src/components/NavBar.vue
index b0198dd..60f8a17 100644
--- a/src/components/NavBar.vue
+++ b/src/components/NavBar.vue
@@ -2,6 +2,7 @@
import {Options, Vue} from 'vue-class-component';
import BootstrapThemeSwitch from '@/components/bootstrapThemeSwitch/BootstrapThemeSwitch.vue';
import AniListUserSearch from '@/components/AniListUserSearch.vue';
+import MangaUpdatesUpdater from '@/components/MangaUpdatesUpdater.vue';
import LocaleSelector from '@/components/locale/LocaleSelector.vue';
@Options({
@@ -10,6 +11,7 @@ import LocaleSelector from '@/components/locale/LocaleSelector.vue';
AniListUserSearch,
BootstrapThemeSwitch,
LocaleSelector,
+ MangaUpdatesUpdater,
},
})
export default class NavBar extends Vue {
@@ -51,7 +53,10 @@ export default class NavBar extends Vue {
diff --git a/src/stores/AniListStore.ts b/src/data/api/AniListApi.ts
similarity index 62%
rename from src/stores/AniListStore.ts
rename to src/data/api/AniListApi.ts
index c53a5cc..ca956fb 100644
--- a/src/stores/AniListStore.ts
+++ b/src/data/api/AniListApi.ts
@@ -1,70 +1,33 @@
-import {Pinia, Store} from 'pinia-class-component';
-import type {User} from '@/models/User';
-import type {MangaListCollection} from '@/models/MangaListCollection';
+import type {AniListUser} from '@/data/models/anilist/AniListUser';
+import {handleJsonResponse} from '@/data/api/ApiUtils';
+import type {AniListMangaListCollection} from '@/data/models/anilist/AniListMangaListCollection';
-@Store({
- id: 'AniListStore',
- name: 'AniListStore',
-})
-export class AniListStore extends Pinia {
- //data
- private x_manga: MangaListCollection | null = null;
- private userNameUpdateTrigger: string | null = null;
- private x_user: User | null = null;
-
- //getter
- get manga(): MangaListCollection | null {
- return this.x_manga;
- }
-
- get userName(): string | null {
- this.userNameUpdateTrigger;
- return window.localStorage.getItem('userName');
- }
-
- get user(): User | null {
- return this.x_user;
- }
-
- //actions
- setUserName(userName: string | null) {
- this.userNameUpdateTrigger = userName;
- if (userName) {
- window.localStorage.setItem('userName', userName ?? '');
- } else {
- window.localStorage.removeItem('userName');
- }
- }
-
- async reload(): Promise
{
- if (!this.userName) {
- return;
- }
-
- let res = await fetch('/graphql', {
+export default class AniListApi {
+ async fetchUser(userName: string): Promise {
+ const res = fetch('/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
'query': 'query($id:Int,$name:String){User(id:$id,name:$name){id name previousNames{name updatedAt}avatar{large}bannerImage about isFollowing isFollower donatorTier donatorBadge createdAt moderatorRoles isBlocked bans options{profileColor restrictMessagesToFollowing}mediaListOptions{scoreFormat}statistics{anime{count meanScore standardDeviation minutesWatched episodesWatched genrePreview:genres(limit:10,sort:COUNT_DESC){genre count}}manga{count meanScore standardDeviation chaptersRead volumesRead genrePreview:genres(limit:10,sort:COUNT_DESC){genre count}}}stats{activityHistory{date amount level}}favourites{anime{edges{favouriteOrder node{id type status(version:2)format isAdult bannerImage title{userPreferred}coverImage{large}startDate{year}}}}manga{edges{favouriteOrder node{id type status(version:2)format isAdult bannerImage title{userPreferred}coverImage{large}startDate{year}}}}characters{edges{favouriteOrder node{id name{userPreferred}image{large}}}}staff{edges{favouriteOrder node{id name{userPreferred}image{large}}}}studios{edges{favouriteOrder node{id name}}}}}}',
- 'variables': {'name': this.userName},
+ 'variables': {'name': userName},
}),
});
- this.x_user = (await res.json()).data.User;
- console.log(this.user);
+ return (await handleJsonResponse(res)).data.User;
+ }
- res = await fetch('/graphql', {
+ async fetchManga(userId: number): Promise {
+ const res = fetch('/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
'query': 'query($userId:Int,$userName:String,$type:MediaType){MediaListCollection(userId:$userId,userName:$userName,type:$type){lists{name isCustomList isCompletedList:isSplitCompletedList entries{...mediaListEntry}}user{id name avatar{large}mediaListOptions{scoreFormat rowOrder animeList{sectionOrder customLists splitCompletedSectionByFormat theme}mangaList{sectionOrder customLists splitCompletedSectionByFormat theme}}}}}fragment mediaListEntry on MediaList{id mediaId status score progress progressVolumes repeat priority private hiddenFromStatusLists customLists advancedScores notes updatedAt startedAt{year month day}completedAt{year month day}media{id title{userPreferred romaji english native}coverImage{extraLarge large}type format status(version:2)episodes volumes chapters averageScore popularity isAdult countryOfOrigin genres bannerImage startDate{year month day}}}',
- 'variables': {'userId': this.user!.id, 'type': 'MANGA'},
+ 'variables': {'userId': userId, 'type': 'MANGA'},
}),
});
- this.x_manga = (await res.json()).data.MediaListCollection;
- console.log(this.manga);
+ return (await handleJsonResponse(res)).data.MediaListCollection;
}
}
\ No newline at end of file
diff --git a/src/data/api/ApiUtils.ts b/src/data/api/ApiUtils.ts
new file mode 100644
index 0000000..3edc2c0
--- /dev/null
+++ b/src/data/api/ApiUtils.ts
@@ -0,0 +1,8 @@
+export function handleJsonResponse(res: Primise): Promise {
+ return res.then(async e => {
+ if (e.status === 200) {
+ return await e.json();
+ }
+ throw new Error('Api error ' + e.status + ': ' + await e.text());
+ });
+}
\ No newline at end of file
diff --git a/src/data/api/MangaUpdatesApi.ts b/src/data/api/MangaUpdatesApi.ts
new file mode 100644
index 0000000..5c2d2b1
--- /dev/null
+++ b/src/data/api/MangaUpdatesApi.ts
@@ -0,0 +1,30 @@
+import {handleJsonResponse} from '@/data/api/ApiUtils';
+
+import type {MangaUpdatesSearchResult} from '@/data/models/mangaupdates/MangaUpdatesSearchResult';
+import type {MangaUpdatesSeries} from '@/data/models/mangaupdates/MangaUpdatesSeries';
+import type {MangaUpdatesSeriesGroups} from '@/data/models/mangaupdates/MangaUpdatesSeriesGroups';
+
+export default class MangaUpdatesApi {
+ search(name: string): Promise {
+ const res = fetch('/mangaupdates/v1/series/search', {
+ method: 'POST',
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify({
+ search: name,
+ stype: 'title',
+ type: 'Manga',
+ }),
+ });
+ return handleJsonResponse(res);
+ }
+
+ get(id: number): Promise {
+ const res = fetch(`/mangaupdates/v1/series/${id}`);
+ return handleJsonResponse(res);
+ }
+
+ groups(id: number): Promise {
+ const res = fetch(`/mangaupdates/v1/series/${id}/groups`);
+ return handleJsonResponse(res);
+ }
+}
diff --git a/src/data/db/AniListDb.ts b/src/data/db/AniListDb.ts
new file mode 100644
index 0000000..2889da9
--- /dev/null
+++ b/src/data/db/AniListDb.ts
@@ -0,0 +1,101 @@
+import {type IDBPDatabase, openDB} from 'idb';
+import type {AniListUser} from '@/data/models/anilist/AniListUser';
+import type {AniListMangaListEntry} from '@/data/models/anilist/AniListMangaListEntry';
+import type {AniListMedia} from '@/data/models/anilist/AniListMedia';
+import type {AniListMangaList} from '@/data/models/anilist/AniListMangaList';
+
+export type AlDb = IDBPDatabase;
+
+export default class AniListDb {
+ private intDb: IDBPDatabase | null = null;
+
+ get db(): IDBPDatabase | null {
+ return this.intDb;
+ }
+
+ static async withDb(fn: (db: AlDb) => Promise | T): Promise {
+ const db = new AniListDb();
+ const idb = await db.open();
+ try {
+ return await fn(idb);
+ } finally {
+ db.close();
+ }
+ }
+
+ async open(): Promise> {
+ this.intDb = await openDB('anilist', 1, {
+ upgrade(db, oldVersion, newVersion, transaction, event) {
+ switch (oldVersion) {
+ case 0:
+ const user = db.createObjectStore('user', {keyPath: 'id'});
+ user.createIndex('id', 'id', {unique: true});
+ user.createIndex('name', 'name', {unique: true});
+
+ const list = db.createObjectStore('list', {keyPath: '_userId_listName'});
+ list.createIndex('_userId', '_userId', {multiEntry: true});
+ list.createIndex('_userId_listName', '_userId_listName', {unique: true}); // TODO multiprop indexes
+
+ const manga = db.createObjectStore('manga', {keyPath: 'id'});
+ manga.createIndex('id', 'id', {unique: true});
+ manga.createIndex('_userId', '_userId', {multiEntry: true});
+ manga.createIndex('_listName', '_listName', {multiEntry: true});
+ manga.createIndex('_userId_listName', '_userId_listName', {multiEntry: true});
+
+ const media = db.createObjectStore('media', {keyPath: 'id'});
+ media.createIndex('id', 'id', {unique: true});
+ //fall through
+ default:
+ break;
+ }
+ },
+ });
+ return this.intDb;
+ }
+
+ close(): void {
+ this.intDb?.close();
+ }
+}
+
+export type AniListDBSchema = {
+ user: {
+ key: 'id',
+ value: AniListUser,
+ indexes: {
+ id: number,
+ name: string
+ }
+ },
+ list: {
+ value: AniListMangaList & {
+ _userId: number,
+ _userId_listName: string
+ },
+ indexes: {
+ _userId: number,
+ _userId_listName: string
+ }
+ },
+ manga: {
+ key: 'id';
+ value: AniListMangaListEntry & {
+ _userId: number,
+ _listName: string,
+ _userId_listName: string
+ },
+ indexes: {
+ id: number,
+ _userId: number,
+ _listName: string,
+ _userId_listName: string
+ }
+ },
+ media: {
+ key: 'id';
+ value: AniListMedia;
+ indexes: {
+ id: number
+ }
+ },
+}
\ No newline at end of file
diff --git a/src/data/db/DbUtil.ts b/src/data/db/DbUtil.ts
new file mode 100644
index 0000000..531c55a
--- /dev/null
+++ b/src/data/db/DbUtil.ts
@@ -0,0 +1,4 @@
+export function strip_(obj: T): T {
+ Object.keys(obj).filter(e => e.startsWith('_')).forEach(k => delete (obj as any)[k]);
+ return obj;
+}
\ No newline at end of file
diff --git a/src/data/db/MangaUpdatesDb.ts b/src/data/db/MangaUpdatesDb.ts
new file mode 100644
index 0000000..d697af9
--- /dev/null
+++ b/src/data/db/MangaUpdatesDb.ts
@@ -0,0 +1,73 @@
+import {type IDBPDatabase, openDB} from 'idb';
+import type {MangaUpdatesSeries} from '@/data/models/mangaupdates/MangaUpdatesSeries';
+import type {MangaUpdatesChapter} from '@/data/models/mangaupdates/MangaUpdatesChapter';
+
+export type MuDb = IDBPDatabase;
+
+export default class MangaUpdatesDb {
+ private intDb: IDBPDatabase | null = null;
+
+ get db(): IDBPDatabase | null {
+ return this.intDb;
+ }
+
+ static async withDb(fn: (db: MuDb) => Promise | T): Promise {
+ const db = new MangaUpdatesDb();
+ const idb = await db.open();
+ try {
+ return await fn(idb);
+ } finally {
+ db.close();
+ }
+ }
+
+ async open(): Promise> {
+ this.intDb = await openDB('mangaupdates', 1, {
+ upgrade(db, oldVersion, newVersion, transaction, event) {
+ switch (oldVersion) {
+ case 0:
+ const relation = db.createObjectStore('relation', {autoIncrement: true});
+ relation.createIndex('unique', ['aniListMediaId', 'mangaUpdatesSeriesId'], {unique: true});
+
+ const media = db.createObjectStore('series', {keyPath: 'series_id'});
+ media.createIndex('series_id', 'series_id', {unique: true});
+
+ const chapter = db.createObjectStore('chapter', {autoIncrement: true});
+ chapter.createIndex('series_id', 'series_id', {multiEntry: true});
+ //fall through
+ default:
+ break;
+ }
+ },
+ });
+ return this.intDb;
+ }
+
+ close(): void {
+ this.intDb?.close();
+ }
+}
+
+export type MangaUpdatesDBSchema = {
+ relation: {
+ key: 'id',
+ value: MangaUpdatesRelation,
+ indexes: {
+ unique: [number, number]
+ }
+ },
+ series: {
+ key: 'series_id';
+ value: MangaUpdatesSeries;
+ indexes: {
+ series_id: number
+ }
+ },
+ chapter: {
+ key: 'id';
+ value: MangaUpdatesChapter;
+ indexes: {
+ series_id: number
+ }
+ },
+}
\ No newline at end of file
diff --git a/src/data/models/anilist/AniListMangaList.ts b/src/data/models/anilist/AniListMangaList.ts
new file mode 100644
index 0000000..f7efa13
--- /dev/null
+++ b/src/data/models/anilist/AniListMangaList.ts
@@ -0,0 +1,8 @@
+import {AniListMangaListEntry} from '@/data/models/anilist/AniListMangaListEntry';
+
+export type AniListMangaList = {
+ name: 'Paused' | 'Completed' | 'Planning' | 'Dropped' | 'Reading',
+ isCustomList: boolean,
+ isCompletedList: boolean,
+ entries: AniListMangaListEntry[]
+}
\ No newline at end of file
diff --git a/src/data/models/anilist/AniListMangaListCollection.ts b/src/data/models/anilist/AniListMangaListCollection.ts
new file mode 100644
index 0000000..862cfd2
--- /dev/null
+++ b/src/data/models/anilist/AniListMangaListCollection.ts
@@ -0,0 +1,6 @@
+import type {AniListMangaList} from '@/data/models/anilist/AniListMangaList';
+
+export type AniListMangaListCollection = {
+ lists: AniListMangaList[]
+}
+
diff --git a/src/data/models/anilist/AniListMangaListEntry.ts b/src/data/models/anilist/AniListMangaListEntry.ts
new file mode 100644
index 0000000..4de6d6b
--- /dev/null
+++ b/src/data/models/anilist/AniListMangaListEntry.ts
@@ -0,0 +1,21 @@
+import type {AniListMedia} from '@/data/models/anilist/AniListMedia';
+
+export type AniListMangaListEntry = {
+ id: number,
+ mediaId: number,
+ status: 'PAUSED' | 'COMPLETED' | 'PLANNING' | 'DROPPED' | 'READING',
+ score: number,
+ progress: number,
+ progressVolumes: number,
+ repeat: number,
+ priority: number,
+ private: boolean,
+ hiddenFromStatusLists: boolean,
+ customLists: any,
+ advancedScores: any,
+ notes: any,
+ updatedAt: number
+ startedAt: any,
+ completedAt: any,
+ media: AniListMedia,
+}
\ No newline at end of file
diff --git a/src/data/models/anilist/AniListMedia.ts b/src/data/models/anilist/AniListMedia.ts
new file mode 100644
index 0000000..e7d94b5
--- /dev/null
+++ b/src/data/models/anilist/AniListMedia.ts
@@ -0,0 +1,26 @@
+export type AniListMedia = {
+ id: number,
+ title: {
+ userPreferred: string,
+ romaji: string,
+ english: string,
+ native: string,
+ }
+ coverImage: {
+ extraLarge: string, //url
+ large: string, //url
+ }
+ type: 'MANGA' | string,
+ format: 'MANGA' | string,
+ status: 'RELEASING' | string,
+ episodes: number | null,
+ volumes: number | null,
+ chapters: number | null,
+ averageScore: number,
+ popularity: number,
+ isAdult: boolean,
+ countryOfOrigin: 'JP' | 'KR' | string,
+ genres: string[],
+ bannerImage: string, //url
+ startDate: any,
+}
\ No newline at end of file
diff --git a/src/models/User.ts b/src/data/models/anilist/AniListUser.ts
similarity index 58%
rename from src/models/User.ts
rename to src/data/models/anilist/AniListUser.ts
index e737c1f..3af8333 100644
--- a/src/models/User.ts
+++ b/src/data/models/anilist/AniListUser.ts
@@ -1,4 +1,4 @@
-export type User = {
+export type AniListUser = {
id: number,
name: string,
//...
diff --git a/src/data/models/mangaupdates/MangaUpdateSearchResult.ts b/src/data/models/mangaupdates/MangaUpdateSearchResult.ts
new file mode 100644
index 0000000..7af4784
--- /dev/null
+++ b/src/data/models/mangaupdates/MangaUpdateSearchResult.ts
@@ -0,0 +1,91 @@
+export type SearchResultRecord = {
+ 'series_id': number,
+ 'title': string,
+ 'url': string,
+ 'description': string,
+ 'image': {
+ 'url': {
+ 'original': string,
+ 'thumb': string
+ },
+ 'height': number,
+ 'width': number
+ },
+ 'type': 'Artbook' | 'Doujinshi' | 'Drama CD' | 'Filipino' | 'Indonesian' | 'Manga' | 'Manhwa' | 'Manhua' | 'Novel'
+ | 'OEL' | 'Thai' | 'Vietnamese' | 'Malaysian' | 'Nordic' | 'French' | 'Spanish',
+ 'year': string,
+ 'bayesian_rating': number,
+ 'rating_votes': number,
+ 'genres': [{
+ 'genre': string
+ }],
+ 'latest_chapter': number,
+ 'rank': {
+ 'position': {
+ 'week': number,
+ 'month': number,
+ 'three_months': number,
+ 'six_months': number,
+ 'year': number
+ },
+ 'old_position': {
+ 'week': number,
+ 'month': number,
+ 'three_months': number,
+ 'six_months': number,
+ 'year': number
+ },
+ 'lists': {
+ 'reading': number,
+ 'wish': number,
+ 'complete': number,
+ 'unfinished': number,
+ 'custom': number
+ }
+ },
+ 'last_updated': {
+ 'timestamp': number,
+ 'as_rfc3339': string,
+ 'as_string': string
+ },
+ 'admin': {
+ 'added_by': {
+ 'user_id': number,
+ 'username': string,
+ 'url': string,
+ 'avatar': {
+ 'id': number,
+ 'url': string,
+ 'height': number,
+ 'width': number
+ },
+ 'time_joined': {
+ 'timestamp': number,
+ 'as_rfc3339': string,
+ 'as_string': string
+ },
+ 'signature': string,
+ 'forum_title': string,
+ 'folding_at_home': true,
+ 'profile': {
+ 'upgrade': {
+ 'requested': true,
+ 'reason': string
+ }
+ },
+ 'stats': {
+ 'forum_posts': number,
+ 'added_authors': number,
+ 'added_groups': number,
+ 'added_publishers': number,
+ 'added_releases': number,
+ 'added_series': number
+ },
+ 'user_group': string,
+ 'user_group_name': string
+ },
+ 'approved': true
+ }
+};
+
+
diff --git a/src/data/models/mangaupdates/MangaUpdatesChapter.ts b/src/data/models/mangaupdates/MangaUpdatesChapter.ts
new file mode 100644
index 0000000..8b94193
--- /dev/null
+++ b/src/data/models/mangaupdates/MangaUpdatesChapter.ts
@@ -0,0 +1,5 @@
+export type MangaUpdatesChapter = {
+ series_id: number,
+ group: string,
+ chapter: number
+}
\ No newline at end of file
diff --git a/src/data/models/mangaupdates/MangaUpdatesRelation.ts b/src/data/models/mangaupdates/MangaUpdatesRelation.ts
new file mode 100644
index 0000000..a863b5d
--- /dev/null
+++ b/src/data/models/mangaupdates/MangaUpdatesRelation.ts
@@ -0,0 +1,5 @@
+export type MangaUpdatesRelation = {
+ aniListMediaId: number,
+ mangaUpdatesSeriesId: number
+}
+
diff --git a/src/data/models/mangaupdates/MangaUpdatesSearchResult.ts b/src/data/models/mangaupdates/MangaUpdatesSearchResult.ts
new file mode 100644
index 0000000..d20d75b
--- /dev/null
+++ b/src/data/models/mangaupdates/MangaUpdatesSearchResult.ts
@@ -0,0 +1,13 @@
+import type {SearchResultRecord} from '@/data/models/mangaupdates/MangaUpdateSearchResult';
+import type {MangaUpdatesSearchResultMetaData} from '@/data/models/mangaupdates/MangaUpdatesSearchResultMetaData';
+
+export type MangaUpdatesSearchResult = {
+ 'total_hits': number,
+ 'page': number,
+ 'per_page': number,
+ 'results': [{
+ 'record': SearchResultRecord,
+ 'hit_title': string,
+ 'metadata': MangaUpdatesSearchResultMetaData
+ }]
+}
\ No newline at end of file
diff --git a/src/data/models/mangaupdates/MangaUpdatesSearchResultMetaData.ts b/src/data/models/mangaupdates/MangaUpdatesSearchResultMetaData.ts
new file mode 100644
index 0000000..86ec6fd
--- /dev/null
+++ b/src/data/models/mangaupdates/MangaUpdatesSearchResultMetaData.ts
@@ -0,0 +1,25 @@
+export type MangaUpdatesSearchResultMetaData = {
+ 'user_list': {
+ 'series': {
+ 'id': number,
+ 'title': string
+ },
+ 'list_id': number,
+ 'list_type': string,
+ 'list_icon': string,
+ 'status': {
+ 'volume': number,
+ 'chapter': number
+ },
+ 'priority': number,
+ 'time_added': {
+ 'timestamp': number,
+ 'as_rfc3339': string,
+ 'as_string': string
+ }
+ },
+ 'user_genre_highlights': [{
+ 'genre': string,
+ 'color': string
+ }]
+}
\ No newline at end of file
diff --git a/src/data/models/mangaupdates/MangaUpdatesSeries.ts b/src/data/models/mangaupdates/MangaUpdatesSeries.ts
new file mode 100644
index 0000000..9314f98
--- /dev/null
+++ b/src/data/models/mangaupdates/MangaUpdatesSeries.ts
@@ -0,0 +1,67 @@
+export type MangaUpdatesSeries = {
+ 'series_id': number,
+ 'title': string,
+ 'url': string,
+ 'associated': { 'title': string }[],
+ 'description': string,
+ 'image': {
+ 'url': {
+ 'original': string,
+ 'thumb': string
+ },
+ 'height': number,
+ 'width': number
+ },
+ 'type': 'Artbook' | 'Doujinshi' | 'Drama CD' | 'Filipino' | 'Indonesian' | 'Manga' | 'Manhwa' | 'Manhua' | 'Novel'
+ | 'OEL' | 'Thai' | 'Vietnamese' | 'Malaysian' | 'Nordic' | 'French' | 'Spanish',
+ 'year': string,
+ 'bayesian_rating': number,
+ 'rating_votes': number,
+ 'genres': { 'genre': string }[],
+ 'categories': { 'series_id': number, 'category': string, 'votes': number, 'votes_plus': number, 'votes_minus': number, 'added_by': number }[],
+ 'latest_chapter': number,
+ 'forum_id': number,
+ 'status': string,
+ 'licensed': true,
+ 'completed': false,
+ 'anime': {
+ 'start': string,
+ 'end': string
+ },
+ 'related_series': { 'relation_id': number, 'relation_type': string, 'related_series_id': number, 'related_series_name': string, 'triggered_by_relation_id': number }[],
+ 'publishers': { 'publisher_name': string, 'publisher_id': number, 'type': string, 'notes': string }[],
+ 'publications': { 'publication_name': string, 'publisher_name': string, 'publisher_id': number }[],
+ 'recommendations': { 'series_name': string, 'series_id': number, 'weight': number }[],
+ 'category_recommendations': { 'series_name': string, 'series_id': number, 'weight': number }[],
+ 'rank': {
+ 'position':
+ {
+ 'week': number,
+ 'month': number,
+ 'three_months': number,
+ 'six_months': number,
+ 'year': number
+ },
+ 'old_position':
+ {
+ 'week': number,
+ 'month': number,
+ 'three_months': number,
+ 'six_months': number,
+ 'year': number
+ },
+ 'lists':
+ {
+ 'reading': number,
+ 'wish': number,
+ 'complete': number,
+ 'unfinished': number,
+ 'custom': number
+ }
+ },
+ 'last_updated': {
+ 'timestamp': number,
+ 'as_rfc3339': string,
+ 'as_string': string
+ }
+}
\ No newline at end of file
diff --git a/src/data/models/mangaupdates/MangaUpdatesSeriesGroups.ts b/src/data/models/mangaupdates/MangaUpdatesSeriesGroups.ts
new file mode 100644
index 0000000..947427c
--- /dev/null
+++ b/src/data/models/mangaupdates/MangaUpdatesSeriesGroups.ts
@@ -0,0 +1,25 @@
+export type MangaUpdatesSeriesGroups = {
+ 'group_list': {
+ 'group_id': number,
+ 'name': string,
+ 'url': string,
+ 'social': {
+ 'site': string,
+ 'facebook': string,
+ 'twitter': string,
+ 'irc': { 'channel': string, 'server': string },
+ 'forum': string,
+ 'discord': string
+ },
+ 'active': true
+ }[],
+ 'release_list': {
+ 'id': number,
+ 'title': string,
+ 'volume': null,
+ 'chapter': string,
+ 'groups': { 'name': string, 'group_id': number }[],
+ 'release_date': string,
+ 'time_added': { 'timestamp': number, 'as_rfc3339': string, 'as_string': string }
+ }[]
+}
\ No newline at end of file
diff --git a/src/data/repository/aniList/AniListMangaRepository.ts b/src/data/repository/aniList/AniListMangaRepository.ts
new file mode 100644
index 0000000..1afa16a
--- /dev/null
+++ b/src/data/repository/aniList/AniListMangaRepository.ts
@@ -0,0 +1,114 @@
+import AniListDb, {type AlDb} from '@/data/db/AniListDb';
+import {strip_} from '@/data/db/DbUtil';
+import type {AniListMangaListEntry} from '@/data/models/anilist/AniListMangaListEntry';
+import type {AniListMedia} from '@/data/models/anilist/AniListMedia';
+import type {AniListMangaList} from '@/data/models/anilist/AniListMangaList';
+
+export default class AniListMangaRepository {
+ async getMangaLists(userId: number): Promise {
+ return await AniListDb.withDb(async db => {
+ return (await db.getAllFromIndex('list', '_userId', userId)).map(strip_);
+ });
+ }
+
+ async getManga(userId: number): Promise