├── anilist_history_scrapper.js ├── README.md └── myanimelist_to_trakt.js /anilist_history_scrapper.js: -------------------------------------------------------------------------------- 1 | let res_final = {}; 2 | let big_res = ''; 3 | 4 | Array.from(document.getElementsByClassName('activity-anime_list')).forEach(entry => { 5 | let timeDiv = entry.getElementsByClassName('time')[0]; 6 | let time = timeDiv.getElementsByTagName('time')[0].getAttribute('title'); 7 | let [s, day, month, year, hour, minute, second, ...r] = time.match(/([0-9]+)\/([0-9]+)\/([0-9]+), ([0-9]+):([0-9]+):([0-9]+)/i); 8 | let date = new Date(year, month - 1, day, hour, minute, second); 9 | 10 | let title = entry.getElementsByClassName('title')[0].textContent.trim(); 11 | let status = entry.getElementsByClassName('status')[0].textContent.replace(title, '').trim(); 12 | 13 | let episodes = status.match(/Watched episode ([0-9]*)( - ([0-9]*)|) of/i); 14 | let EPISODES_LENGHTS = [{}]; 15 | 16 | if (!(title in res_final)) 17 | res_final[title] = ''; 18 | 19 | if (episodes) { 20 | let [t, ep1, m, ep2, ...q] = episodes; 21 | ep2 = (ep2 === undefined) ? ep1 : ep2; 22 | let ep_length = EPISODES_LENGHTS[title] || 24; 23 | 24 | for (let ep = parseInt(ep2); ep >= parseInt(ep1); ep--) { 25 | date.setMinutes(date.getMinutes() - ep_length); 26 | res_final[title] += `Ep ${ep}, watched on ${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()} at ${("0" + date.getHours()).slice(-2)}:${("0" + date.getMinutes()).slice(-2)} Remove\n`; 27 | } 28 | } 29 | if (status === 'Completed') { 30 | res_final[title] += `Ep Final, watched on ${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()} at ${("0" + date.getHours()).slice(-2)}:${("0" + date.getMinutes()).slice(-2)} Remove\n`; 31 | } 32 | }); 33 | 34 | Object.keys(res_final).forEach(title => { 35 | if (res_final[title].split('\n').length > 1) 36 | big_res += `${title} Episode Details\n${res_final[title]}\n`; 37 | }); 38 | 39 | console.log(big_res); 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # trakt-anime-history 2 | 3 | Let's move back MyAnimeList and AniList history to trakt.tv to centralize it ! 4 | 5 | *This is a very basic script that I wrote for a personal usage. It **may break**, it's **far from perfect** and you'll sometimes have to edit stuff because many things are not properly checked when running the code. This simply makes it faster than doing all of this manually for each episode.* 6 | 7 | These scripts allow to **copy your anime history** over to trakt.tv by synchronizing the exact time you watched each episode. This will work only if you are **scroblling your anime** with a third party program, or manually add them to your anime list **each time you finish an anime**. 8 | 9 | ## Setup 10 | 11 | You'll have to create a trakt.tv application at https://trakt.tv/oauth/applications. Then, copy your user key and secret, and paste them where appropriate. You should also get a trakt.tv access token by making the appropriate call. It will be valid for a year so calling it once should be enough :) 12 | 13 | ## From MyAnimeList 14 | 15 | You'll have to retrieve each anime's history **by hand**, since MAL doesn't have an API for that. 16 | 17 | Navigate to https://myanimelist.net/ajaxtb.php?detailedaid=ANIME_ID for each anime in your anime list in order to retrieve the history for that specific anime. You'll get something that looks like this : 18 | 19 | ```Amanchu! Episode Details 20 | Ep 3, watched on 05/25/2019 at 18:42 Remove 21 | Ep 2, watched on 05/25/2019 at 18:20 Remove 22 | Ep 1, watched on 05/25/2019 at 18:00 Remove 23 | ``` 24 | 25 | Then, save this in a file and simply run the node application. Check the console to verify if all episodes were added or not. If there are issues, most of the time, the problem rises from trakt.tv having trouble with some titles. Try to juggle a bit and you should manage to correct the issue. 26 | 27 | *If the anime happens to be a sequel, you'll have to manually input the season number as trakt.tv keeps everything under the same show (which makes more sens in my opinion).* 28 | 29 | ## From AniList 30 | 31 | As with MyAnimeList, no API is provided to retrieve you history. However, it will be way less tedious than MAL to retrieve your history. 32 | 33 | This time around, you'll have to go to your AniList profile and load every activity by clicking on the "Show more" button until you loaded everything. Then, in the console, simply use the AniList JS script. 34 | 35 | You'll get a long string formatted the same way as the MAL history. You can then reuse the MAL script by iterating over each anime. This should be way faster than importing from MAL. 36 | 37 | ## Endnotes 38 | 39 | This was a small project that I started because I had a period where I mostly watched anime. Now that I'm starting to juggle a bit between various medias, I wanted to have a place where every shows I watch rest, for instance to have a centralized calendar of releases. I already used trakt.tv, so this is where I chose to scrobble my anime (as well as on AniList, it's still nice to have a distinct service just for that). 40 | 41 | Since I used [Taiga](https://taiga.moe/) in the past (an awesome application to automatically scrobble anime to MAL/AniList/Kitsu), my MAL and AniList history were almost complete. I wanted to keep that history with me whichever service I use, and this motivated me to export this information to trakt.tv and their awesome API. 42 | 43 | I'm not sure if it will ever be useful to anyone else, but if it is, don't hesitate to tell me, I'd be happy to know :) 44 | 45 | I don't plan to work on this ever again since it already helped me a lot to move my history and saved me a lot of time. Feel free to use it, change it, eat it as you please ! 46 | -------------------------------------------------------------------------------- /myanimelist_to_trakt.js: -------------------------------------------------------------------------------- 1 | let fs = require('fs'); 2 | let request = require('request'); 3 | let levenshtein = require('js-levenshtein'); 4 | 5 | let TRAKT_API_KEY = ''; // Found at https://trakt.tv/oauth/applications 6 | let TRAKT_API_SECRET = ''; // Found at https://trakt.tv/oauth/applications 7 | let TRAKT_TOKEN = ''; // Call getTraktToken() once and write down the token 8 | 9 | String.prototype.capitalize = function () { 10 | return this.charAt(0).toUpperCase() + this.slice(1) 11 | }; 12 | 13 | // Format history to a more schematized format 14 | let formatHistory = (str) => { 15 | let lines = str.split(/\r?\n/); 16 | let title = lines[0].match(/(.+) Episode Details/i)[1]; 17 | let episodes_with_time = lines.slice(1, -1).map(line => { 18 | let [s, ep, month, day, year, hour, minute, ...r] = line.match(/Ep ([0-9]+), watched on ([0-9]+)\/([0-9]+)\/([0-9]+) at ([0-9]+):([0-9]+) Remove/i); 19 | let date = new Date(year, month - 1, day, hour, minute); 20 | return { episode: parseInt(ep), timestamp: date }; 21 | }); 22 | return { title: title, timestamps: episodes_with_time }; 23 | }; 24 | 25 | // Call to get a trakt token ; save it after the call 26 | let getTraktToken = () => { 27 | return new Promise(resolve => { 28 | request.post('https://api.trakt.tv/oauth/device/code', { 29 | headers: { 'Content-Type': 'application/json' }, 30 | body: `{ "client_id": "${TRAKT_API_KEY}" }` 31 | }, (error, response, body) => { 32 | let infos = JSON.parse(body); 33 | console.log(`Open this link in the next 10s : ${infos.verification_url}`); 34 | console.log(`Type this code : ${infos.user_code}`); 35 | resolve(JSON.parse(body)); 36 | }); 37 | }).then(device_infos => 38 | new Promise(resolve => { setTimeout(() => resolve(device_infos), device_infos.interval * 2 * 1000) }) 39 | ).then(device_infos => { 40 | request.post('https://api.trakt.tv/oauth/device/token', { 41 | headers: { 'Content-Type': 'application/json' }, 42 | body: `{ 43 | "code": "${device_infos.device_code}", 44 | "client_id": "${TRAKT_API_KEY}", 45 | "client_secret": "${TRAKT_API_SECRET}" 46 | }` 47 | }, (error, response, body) => { 48 | let infos = JSON.parse(body); 49 | console.log(`Write down this token : ${infos.access_token}`); 50 | }); 51 | }); 52 | }; 53 | 54 | // Generic AniList API call 55 | let callAniListAPI = (query, variables) => { 56 | return new Promise(resolve => { 57 | request.post('https://graphql.anilist.co', { 58 | headers: { 59 | 'Content-Type': 'application/json', 60 | 'Accept': 'application/json', 61 | }, 62 | body: JSON.stringify({ 63 | query: query, 64 | variables: variables 65 | }) 66 | }, function (error, response, body) { 67 | if (response.statusCode == 200 && !error) 68 | resolve(JSON.parse(body)); 69 | }); 70 | }); 71 | }; 72 | 73 | // Get english title of an anime via AniList 74 | let getEnglishTitle = (jp_title) => { 75 | let query = ` 76 | query ($title: String, $type: MediaType) { 77 | Media(search: $title, type: $type) { 78 | id 79 | title { 80 | romaji 81 | english 82 | native 83 | } 84 | } 85 | }`; 86 | let variables = { 87 | "title": jp_title, 88 | "type": "ANIME" 89 | }; 90 | 91 | return callAniListAPI(query, variables).then(infos => infos.data.Media.title.english || jp_title); 92 | }; 93 | 94 | // Generic Trakt API call 95 | let callTraktAPI = (url) => { 96 | return new Promise(resolve => { 97 | request.get(url, { 98 | headers: { 99 | 'Content-Type': 'application/json', 100 | 'trakt-api-version': '2', 101 | 'trakt-api-key': TRAKT_API_KEY 102 | } 103 | }, function (error, response, body) { 104 | if (response.statusCode == 200 && !error) 105 | resolve(JSON.parse(body)); 106 | }); 107 | }); 108 | }; 109 | 110 | // Retrieve show or movie informations 111 | let getTraktShowOrMovie = (title, type) => 112 | callTraktAPI(`https://api.trakt.tv/search/${type}?query=${title}`).then(infos => { 113 | switch (type) { 114 | case 'show': 115 | return infos 116 | .filter(info => info.show.year !== null) 117 | .sort((a, b) => a.show.year > b.show.year) 118 | .sort((a, b) => levenshtein(a.show.title.toLowerCase(), b.show.title.toLowerCase())) 119 | .filter((info, idx, arr) => arr.length == 1 || levenshtein(title.toLowerCase(), info.show.title.toLowerCase()) <= 2) 120 | .sort((a, b) => a.score > b.score)[0] 121 | case 'movie': 122 | return infos 123 | .filter(info => info.movie.year !== null) 124 | .sort((a, b) => a.movie.year > b.movie.year) 125 | .sort((a, b) => levenshtein(a.movie.title.toLowerCase(), b.movie.title.toLowerCase())) 126 | .filter((info, idx, arr) => arr.length == 1 || levenshtein(title.toLowerCase(), info.movie.title.toLowerCase()) <= 2) 127 | .sort((a, b) => a.score > b.score)[0] 128 | } 129 | }); 130 | 131 | // Retrieve season information 132 | let getTraktSeason = (trakt_id, show_title, full_title) => 133 | callTraktAPI(`https://api.trakt.tv/shows/${trakt_id}/seasons?extended=full`).then(infos => 134 | infos.filter(info => info.number != 0) 135 | ); 136 | 137 | // Retrieve episode information 138 | let getTraktEpisode = (trakt_id, season, episode) => callTraktAPI(`https://api.trakt.tv/shows/${trakt_id}/seasons/${season}/episodes/${episode}`); 139 | 140 | // Update on trakt based on multiple episodes of a serie / a movie 141 | let updateOnTrakt = async (mal_infos, override_season = -1, ANIME_TYPE = 'show') => { 142 | let mal_title = mal_infos.title; 143 | console.log(`Anime : ${mal_title}`); 144 | 145 | let english_title = await getEnglishTitle(mal_title); 146 | console.log(`English Title : ${english_title}`); 147 | 148 | let anime = await getTraktShowOrMovie(english_title, ANIME_TYPE); 149 | let title = (ANIME_TYPE == 'show') ? anime.show.title : anime.movie.title; 150 | let trakt_slug = (ANIME_TYPE == 'show') ? anime.show.ids.slug : anime.movie.ids.slug; 151 | console.log(`${ANIME_TYPE.capitalize()} : ${title}`); 152 | console.log(`Link : https://trakt.tv/shows/${trakt_slug}`); 153 | 154 | let season_infos = await getTraktSeason(trakt_slug, title, mal_title); 155 | let season = (override_season != -1) ? override_season : season_infos[0].number; 156 | console.log(`Season : ${season}`); 157 | 158 | let ep = await Promise.all(mal_infos.timestamps.map(mal_timestamp_infos => { 159 | let episode = mal_timestamp_infos.episode; 160 | let timestamp = mal_timestamp_infos.timestamp; 161 | return getTraktEpisode(trakt_slug, season, episode).then(episode_infos => { 162 | return { 163 | "watched_at": timestamp, 164 | "ids": { "trakt": episode_infos.ids.trakt } 165 | }; 166 | }); 167 | })); 168 | let body = { episodes: ep }; 169 | 170 | request.post('https://api.trakt.tv/sync/history', { 171 | headers: { 172 | 'Content-Type': 'application/json', 173 | 'Authorization': `Bearer ${TRAKT_TOKEN}`, 174 | 'trakt-api-version': '2', 175 | 'trakt-api-key': TRAKT_API_KEY 176 | }, 177 | body: JSON.stringify(body) 178 | }, (error, response, body) => { 179 | let r = JSON.parse(body); 180 | console.log(`Added episodes : ${r.added.episodes}`); 181 | console.log(`Missing episodes : ${"None" || r.not_found.episodes}`); 182 | }); 183 | }; 184 | 185 | // getTraktToken(); 186 | let history = fs.readFileSync('anime_history.txt', 'utf-8'); 187 | let infos = formatHistory(history); 188 | updateOnTrakt(infos); 189 | --------------------------------------------------------------------------------