Skip to content

Commit 1f092b6

Browse files
committed
Complete working TS port
1 parent 311c596 commit 1f092b6

File tree

12 files changed

+2691
-1742
lines changed

12 files changed

+2691
-1742
lines changed

douki.user.js

Lines changed: 649 additions & 429 deletions
Large diffs are not rendered by default.

package-lock.json

Lines changed: 1810 additions & 1121 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,16 @@
1313
"devDependencies": {
1414
"@types/node": "^7.0.29",
1515
"@types/pad": "^1.0.0",
16-
"@types/webpack": "^2.2.15",
16+
"@types/tapable": "^1.0.4",
17+
"@types/webpack": "^4.4.22",
1718
"css-loader": "^0.28.4",
1819
"jsonschema": "^1.1.1",
1920
"pad": "^1.1.0",
2021
"style-loader": "^0.18.2",
21-
"ts-loader": "^2.1.0",
22-
"ts-node": "^3.0.6",
23-
"typescript": "^2.3.4",
24-
"webpack": "^2.6.1"
22+
"ts-loader": "^5.3.2",
23+
"ts-node": "^7.0.1",
24+
"typescript": "^2.9.2",
25+
"webpack": "^4.28.3",
26+
"webpack-cli": "^3.1.2"
2527
}
26-
}
28+
}

src/Anilist.ts

Lines changed: 3 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,5 @@
11
import * as Log from './Log';
2-
3-
/*
4-
Anilist response is as follows:
5-
data: {
6-
anime: {
7-
lists: [
8-
{
9-
entries: [
10-
{entry}
11-
]
12-
}
13-
]
14-
},
15-
manga: {
16-
lists: [
17-
{
18-
entries: [
19-
{entry}
20-
]
21-
}
22-
]
23-
},
24-
}
25-
*/
26-
27-
type AnilistDate = {
28-
year: number
29-
month: number
30-
day: number
31-
}
32-
33-
type BaseEntry = {
34-
status: string
35-
score: number
36-
progress: number
37-
progressVolumes: number
38-
startedAt: AnilistDate
39-
completedAt: AnilistDate
40-
repeat: number
41-
}
42-
43-
type Entry = BaseEntry & {
44-
media: {
45-
idMal: number
46-
title: {
47-
romaji: string
48-
}
49-
}
50-
}
51-
52-
type MediaList = {
53-
entries: Array<Entry>
54-
[key: string]: Array<Entry>
55-
}
56-
57-
type MediaListCollection = {
58-
lists: MediaList
59-
}
60-
61-
type AnilistResponse = {
62-
anime: MediaListCollection
63-
manga: MediaListCollection
64-
[key: string]: MediaListCollection
65-
}
66-
67-
type FormattedEntry = BaseEntry & {
68-
type: string
69-
id: number
70-
title: string
71-
}
72-
73-
type DoukiAnilistData = {
74-
anime: Array<FormattedEntry>
75-
manga: Array<FormattedEntry>
76-
}
2+
import { AnilistEntry, MediaList, FormattedEntry, DoukiAnilistData } from './Types';
773

784
const flatten = (obj: MediaList) =>
795
// Outer reduce concats arrays built by inner reduce
@@ -83,7 +9,7 @@ const flatten = (obj: MediaList) =>
839
// @ts-ignore
8410
acc2.concat(obj[list][item]), [])), []);
8511

86-
const uniqify = (arr: Array<Entry>) => {
12+
const uniqify = (arr: Array<AnilistEntry>) => {
8713
const seen = new Set();
8814
return arr.filter(item => (seen.has(item.media.idMal) ? false : seen.add(item.media.idMal)));
8915
};
@@ -169,7 +95,7 @@ const fetchList = (userName: string) =>
16995
manga: uniqify(flatten(res.manga.lists)),
17096
}));
17197

172-
const sanitize = (item: Entry, type: string): FormattedEntry => ({
98+
const sanitize = (item: AnilistEntry, type: string): FormattedEntry => ({
17399
type,
174100
progress: item.progress,
175101
progressVolumes: item.progressVolumes,

src/Dom.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export const addImportForm = (syncFn: Function) => {
7575
const addImportFormEventListeners = (syncFn: Function) => {
7676
const importButton = document.querySelector(id(DOUKI_IMPORT_BUTTON_ID));
7777
importButton && importButton.addEventListener('click', function (e) {
78-
syncFn();
78+
syncFn(e);
7979
});
8080

8181
const textBox = document.querySelector(id(ANILIST_USERNAME_ID)) as HTMLInputElement;
@@ -124,5 +124,26 @@ const setLocalStorageSetting = (setting: string, value: string) => {
124124

125125
export const getDateSetting = (): string => {
126126
const dateSetting = document.querySelector(id(DATE_SETTING_ID)) as HTMLSelectElement;
127-
return dateSetting && dateSetting.value;
127+
if (!dateSetting) throw new Error('Unable to get date setting');
128+
return dateSetting.value;
129+
}
130+
131+
export const getCSRFToken = (): string => {
132+
const csrfTokenMeta = document.querySelector('meta[name~="csrf_token"]');
133+
if (!csrfTokenMeta) throw new Error('Unable to get CSRF token - no meta element');
134+
const csrfToken = csrfTokenMeta.getAttribute('content');
135+
if (!csrfToken) throw new Error('Unable to get CSRF token - no content attribute');
136+
return csrfToken;
137+
}
138+
139+
export const getMALUsername = () => {
140+
const malUsernameElement = document.querySelector('.header-profile-link') as HTMLDivElement;
141+
if (!malUsernameElement) return null;
142+
return malUsernameElement.innerText;
143+
}
144+
145+
export const getAnilistUsername = () => {
146+
const anilistUserElement = document.querySelector('#douki-anilist-username') as HTMLInputElement;
147+
if (!anilistUserElement) throw new Error('Unable to get Anilist username');
148+
return anilistUserElement.value;
128149
}

src/Log.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { SYNC_LOG_ID, ERROR_LOG_ID } from './const';
2-
import { id } from './util';
2+
import { id, getOperationDisplayName } from './util';
33

44
const getSyncLog = (): Element | null => document.querySelector(id(SYNC_LOG_ID));
55
const getErrorLog = (): Element | null => document.querySelector(id(ERROR_LOG_ID));
6+
const getCountLog = (operation: string, type: string): Element | null => document.querySelector(id(`douki-${operation}-${type}-items`));
67

78
const clearErrorLog = () => {
89
const errorLog = getErrorLog();
@@ -40,3 +41,15 @@ export const info = (msg: string) => {
4041
console.info(msg);
4142
}
4243
}
44+
45+
export const addCountLog = (operation: string, type: string, max: number) => {
46+
const opName = getOperationDisplayName(operation);
47+
const logId = `douki-${operation}-${type}-items`;
48+
info(`${opName} <span id="${logId}">0</span> of ${max} ${type} items.`);
49+
}
50+
51+
export const updateCountLog = (operation: string, type: string, count: number) => {
52+
const countLog = getCountLog(operation, type) as HTMLSpanElement;
53+
if (!countLog) return;
54+
countLog.innerHTML = `${count}`;
55+
}

src/MAL.ts

Lines changed: 72 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,29 @@
1-
const getMALHashMap = async (type: string, username: string, list: Array<any> = [], page = 1): Promise<any> => {
1+
import { sleep, getOperationDisplayName } from './util';
2+
import * as Log from './Log';
3+
import * as Dom from './Dom';
4+
import { MALHashMap, MALItem, MediaDate, FormattedEntry, FullDataEntry } from './Types';
5+
6+
const createMALHashMap = (malList: Array<MALItem>, type: string): MALHashMap => {
7+
const hashMap: MALHashMap = {};
8+
malList.forEach(item => {
9+
hashMap[item[`${type}_id`]] = item;
10+
});
11+
return hashMap;
12+
}
13+
14+
const getMALHashMap = async (type: string, username: string, list: Array<MALItem> = [], page = 1): Promise<MALHashMap> => {
215
const offset = (page - 1) * 300;
3-
const nextList = await fetch(`https://myanimelist.net/${type}list/${username}/load.json?offset=${offset}&status=7`).then(res => res.json());
16+
const nextList = await fetch(`https://myanimelist.net/${type}list/${username}/load.json?offset=${offset}&status=7`)
17+
.then(res => res.json());
418
if (nextList && nextList.length) {
519
await sleep(1000);
620
return getMALHashMap(type, username, [...list, ...nextList], page + 1);
721
}
822
Log.info(`Fetched MyAnimeList ${type} list.`);
9-
const fullList = [...list, ...nextList];
10-
return createMALHashMap(fullList, type);
23+
return createMALHashMap([...list, ...nextList], type);
1124
}
1225

13-
const malEdit = (type, data) =>
26+
const malEdit = (type: string, data: MALItem) =>
1427
fetch(`https://myanimelist.net/ownlist/${type}/edit.json`, {
1528
method: 'post',
1629
body: JSON.stringify(data)
@@ -20,7 +33,7 @@ const malEdit = (type, data) =>
2033
throw new Error(JSON.stringify(data));
2134
});
2235

23-
const malAdd = (type, data) =>
36+
const malAdd = (type: string, data: MALItem) =>
2437
fetch(`https://myanimelist.net/ownlist/${type}/add.json`, {
2538
method: 'post',
2639
headers: {
@@ -35,7 +48,7 @@ const malAdd = (type, data) =>
3548
throw new Error(JSON.stringify(data));
3649
});
3750

38-
const getStatus = (status) => {
51+
const getStatus = (status: string) => {
3952
// MAL status: 1/watching, 2/completed, 3/onhold, 4/dropped, 6/plantowatch
4053
// MAL handles REPEATING as a boolean, and keeps status as COMPLETE
4154
switch (status.trim()) {
@@ -55,7 +68,7 @@ const getStatus = (status) => {
5568
}
5669
}
5770

58-
const buildDateString = (date) => {
71+
const buildDateString = (date: MediaDate) => {
5972
if (date.month === 0 && date.day === 0 && date.year === 0) return null;
6073
const dateSetting = Dom.getDateSetting();
6174
const month = `${String(date.month).length < 2 ? '0' : ''}${date.month}`;
@@ -67,7 +80,7 @@ const buildDateString = (date) => {
6780
return `${day}-${month}-${year}`;
6881
}
6982

70-
const createMALData = (anilistData, malData, csrf_token) => {
83+
const createMALData = (anilistData: FormattedEntry, malData: MALItem, csrf_token: string): MALItem => {
7184
const status = getStatus(anilistData.status);
7285
const result = {
7386
status,
@@ -83,7 +96,7 @@ const createMALData = (anilistData, malData, csrf_token) => {
8396
month: anilistData.startedAt.month || 0,
8497
day: anilistData.startedAt.day || 0
8598
},
86-
};
99+
} as MALItem;
87100

88101
result[`${anilistData.type}_id`] = anilistData.id;
89102

@@ -106,9 +119,9 @@ const createMALData = (anilistData, malData, csrf_token) => {
106119
result.num_read_volumes = malData.manga_num_volumes || 0;
107120
}
108121
}
122+
} else {
109123
// Non-completed item; use Anilist's counts
110124
// Note the possibility that this count could be higher than MAL's max; see if that creates problems
111-
} else {
112125
if (anilistData.type === 'anime') {
113126
result.num_watched_episodes = anilistData.progress || 0;
114127
} else {
@@ -119,15 +132,7 @@ const createMALData = (anilistData, malData, csrf_token) => {
119132
return result;
120133
};
121134

122-
const createMALHashMap = (malList, type) => {
123-
const hashMap = {};
124-
malList.forEach(item => {
125-
hashMap[item[`${type}_id`]] = item;
126-
});
127-
return hashMap;
128-
}
129-
130-
const shouldUpdate = (mal, al) =>
135+
const shouldUpdate = (mal: MALItem, al: MALItem) =>
131136
Object.keys(al).some(key => {
132137
switch (key) {
133138
case 'csrf_token':
@@ -162,23 +167,14 @@ const shouldUpdate = (mal, al) =>
162167
// In certain cases the next two values will be missing from the MAL data and trying to update them will do nothing.
163168
// To avoid a meaningless update every time, skip it if undefined on MAL
164169
case 'num_watched_times':
165-
{
166-
if (!mal.hasOwnProperty('num_watched_times')) {
167-
return false;
168-
}
169-
if (al[key] !== mal[key]) {
170-
return true;
171-
};
172-
return false;
173-
}
174170
case 'num_read_times':
175171
{
176-
if (!mal.hasOwnProperty('num_read_times')) {
172+
if (!mal.hasOwnProperty(key)) {
177173
return false;
178174
}
179175
if (al[key] !== mal[key]) {
180176
return true;
181-
}
177+
};
182178
return false;
183179
}
184180
default:
@@ -194,3 +190,48 @@ const shouldUpdate = (mal, al) =>
194190
}
195191
}
196192
});
193+
194+
const syncList = async (type: string, list: Array<FullDataEntry>, operation: string) => {
195+
if (!list || !list.length) {
196+
return;
197+
}
198+
Log.addCountLog(operation, type, list.length);
199+
let itemCount = 0;
200+
// This uses malEdit() for 'completed' as well
201+
const fn = operation === 'add' ? malAdd : malEdit;
202+
for (let item of list) {
203+
await sleep(500);
204+
try {
205+
await fn(type, item.malData);
206+
itemCount++;
207+
Log.updateCountLog(operation, type, itemCount);
208+
} catch (e) {
209+
console.error(e);
210+
Log.info(`Error for ${type} <a href="https://myanimelist.net/${type}/${item.id}" target="_blank" rel="noopener noreferrer">${item.title}</a>. Try adding or updating it manually.`);
211+
}
212+
}
213+
}
214+
215+
export const syncType = async (type: string, anilistList: Array<FormattedEntry>, malUsername: string, csrfToken: string) => {
216+
Log.info(`Fetching MyAnimeList ${type} list...`);
217+
let malHashMap = await getMALHashMap(type, malUsername);
218+
let alPlusMal = anilistList.map(item => Object.assign({}, item, {
219+
malData: createMALData(item, malHashMap[item.id], csrfToken),
220+
})) as Array<FullDataEntry>;
221+
222+
const addList = alPlusMal.filter(item => !malHashMap[item.id]);
223+
await syncList(type, addList, 'add');
224+
225+
// Refresh list to get episode/chapter counts of new completed items
226+
Log.info(`Refreshing MyAnimeList ${type} list...`);
227+
malHashMap = await getMALHashMap(type, malUsername);
228+
alPlusMal = anilistList.map(item => Object.assign({}, item, {
229+
malData: createMALData(item, malHashMap[item.id], csrfToken),
230+
}));
231+
const updateList = alPlusMal.filter(item => {
232+
const malItem = malHashMap[item.id];
233+
if (!malItem) return false;
234+
return shouldUpdate(malItem, item.malData)
235+
});
236+
await syncList(type, updateList, 'edit');
237+
};

0 commit comments

Comments
 (0)