├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── FUNDING.yml ├── .gitignore ├── .nvmrc ├── .postcssrc.js ├── .prettierignore ├── .prettierrc ├── README.md ├── babel.config.js ├── backend ├── .gitignore ├── calendar.js ├── docker-compose.yml ├── error-handler.js ├── http-utils.js ├── package-lock.json ├── package.json ├── providers │ ├── Aniwatch.js │ └── providers.js ├── response.html ├── server.js └── utils │ └── strings.js ├── dist └── spa │ ├── chibi.png │ ├── css │ ├── 2.bc3cfb91.css │ ├── 3.cbb1f13c.css │ ├── 4.f4c9749e.css │ ├── app.aa8f7688.css │ └── vendor.2dcdf556.css │ ├── favicon.ico │ ├── fonts │ ├── KFOkCnqEu92Fr1MmgVxIIzQ.68bb21d0.woff │ ├── KFOlCnqEu92Fr1MmEU9fBBc-.48af7707.woff │ ├── KFOlCnqEu92Fr1MmSU5fBBc-.c2f7ab22.woff │ ├── KFOlCnqEu92Fr1MmWUlfBBc-.77ecb942.woff │ ├── KFOlCnqEu92Fr1MmYUtfBBc-.f5677eb2.woff │ ├── KFOmCnqEu92Fr1Mu4mxM.f1e2a767.woff │ ├── fa-brands-400.2285773e.woff │ ├── fa-brands-400.d878b0a6.woff2 │ ├── fa-regular-400.7a333762.woff2 │ ├── fa-regular-400.bb58e57c.woff │ ├── fa-solid-900.1551f4f6.woff2 │ ├── fa-solid-900.eeccf4f6.woff │ ├── flUhRq6tzZclQEJ-Vdg-IuiaDsNa.4d73cb90.woff │ └── flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.0383092b.woff2 │ ├── icons │ ├── animeflv.ico │ ├── animepahe.png │ ├── crunchyroll.png │ ├── favicon-128x128.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ ├── monoschinos.ico │ └── youtube.png │ ├── index.html │ ├── js │ ├── 10.6910009d.js │ ├── 11.d74a3687.js │ ├── 12.92284c22.js │ ├── 13.4d6a33f0.js │ ├── 14.a0442d14.js │ ├── 15.601bcce8.js │ ├── 16.ee88b894.js │ ├── 17.0d3a8f1c.js │ ├── 18.f8da8327.js │ ├── 19.1e9e23d1.js │ ├── 2.ec70d99d.js │ ├── 20.0aeafe02.js │ ├── 3.f88a54a3.js │ ├── 4.e5f9f4f4.js │ ├── 5.f58dc392.js │ ├── 6.31eb4eef.js │ ├── 7.c01ad7d9.js │ ├── 8.49fba52f.js │ ├── 9.e43c0c97.js │ ├── app.fb2425d9.js │ └── vendor.3787859d.js │ ├── kofi.png │ ├── mal.png │ └── old │ ├── FontSiteSans-Cond-webfont.woff │ ├── background.jpg │ ├── http.js │ ├── index.css │ ├── index.html │ ├── index.js │ ├── load-fig.gif │ ├── mal.jpg │ ├── storage.js │ └── utils.js ├── js └── app.0ec75656.js ├── jsconfig.json ├── old ├── FontSiteSans-Cond-webfont.woff ├── background.jpg ├── http.js ├── index.css ├── index.html ├── index.js ├── load-fig.gif ├── mal.jpg ├── storage.js └── utils.js ├── package.json ├── public ├── chibi.png ├── favicon.ico ├── icons │ ├── animeflv.ico │ ├── animepahe.png │ ├── crunchyroll.png │ ├── favicon-128x128.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ ├── monoschinos.ico │ └── youtube.png ├── kofi.png └── mal.png ├── quasar.conf.js ├── redirect ├── README.md ├── favicon.ico ├── index.html └── style.css ├── src ├── App.vue ├── README.md ├── api │ ├── API.js │ ├── Backend.js │ ├── MALSync.js │ └── MyAnimeList.js ├── assets │ └── sad.svg ├── boot │ ├── components.js │ └── i18n.js ├── components │ ├── About.vue │ ├── AnimeEpisode.vue │ ├── AnimeSettings.vue │ ├── Avatar.vue │ ├── Back.vue │ ├── CalendarButton.vue │ ├── DynamicButton.vue │ ├── ItemButton.vue │ ├── LanguageSelect.vue │ ├── PasswordDialog.vue │ ├── ProviderSelect.vue │ ├── ResetButton.vue │ ├── StatusSelect.vue │ ├── SupportMe.vue │ ├── TitleSelect.vue │ └── UserSearch.vue ├── css │ ├── app.scss │ └── quasar.variables.scss ├── i18n │ ├── en │ │ └── index.js │ ├── es │ │ └── index.js │ └── index.js ├── index.template.html ├── layouts │ └── MainLayout.vue ├── mixins │ ├── bind.js │ ├── configuration.js │ └── keyboard.js ├── model │ ├── Anime.js │ └── providers │ │ ├── AnimeFLV.js │ │ ├── AnimeFenix.js │ │ ├── AnimeHeaven.js │ │ ├── AnimeID.js │ │ ├── AnimeMovil2.js │ │ ├── AnimeSuge.js │ │ ├── Animeflix.js │ │ ├── Animepahe.js │ │ ├── Crunchyroll.js │ │ ├── FeelingLucky.js │ │ ├── Gogoanime.js │ │ ├── MonosChinos.js │ │ ├── MyAnimeList.js │ │ ├── Netflix.js │ │ ├── NineAnime.js │ │ ├── Provider.js │ │ ├── YouTube.js │ │ ├── YugenAnime.js │ │ ├── Zoro.js │ │ └── jkAnime.js ├── pages │ ├── Error404.vue │ └── Index.vue ├── router │ ├── index.js │ └── routes.js ├── store │ ├── index.js │ ├── store-flag.d.ts │ └── store │ │ ├── actions.js │ │ ├── getters.js │ │ ├── index.js │ │ ├── mutations.js │ │ └── state.js └── utils │ ├── errors.js │ ├── strings.js │ └── subscription.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /src-bex/www 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | 4 | parserOptions: { 5 | parser: 'babel-eslint', 6 | sourceType: 'module', 7 | }, 8 | 9 | env: { 10 | browser: true, 11 | es6: true, 12 | node: true, 13 | }, 14 | 15 | extends: [ 16 | // https://eslint.vuejs.org/rules/#priority-a-essential-error-prevention 17 | 'plugin:vue/recommended', 18 | 'airbnb-base', 19 | 'prettier', 20 | 'prettier/vue', 21 | ], 22 | 23 | // required to lint *.vue files 24 | plugins: ['prettier', 'vue'], 25 | 26 | globals: { 27 | ga: true, // Google Analytics 28 | cordova: true, 29 | __statics: true, 30 | process: true, 31 | Capacitor: true, 32 | chrome: true, 33 | }, 34 | 35 | // add your custom rules here 36 | rules: { 37 | 'max-len': [ 38 | 'warn', 39 | { 40 | code: 120, 41 | ignoreUrls: true, 42 | ignoreComments: true, 43 | ignoreStrings: true, 44 | ignoreTemplateLiterals: true, 45 | }, 46 | ], 47 | 48 | 'prefer-destructuring': 'off', // 'error', { object: true, array: false } 49 | 50 | 'vue/component-name-in-template-casing': ['error', 'PascalCase'], 51 | 'vue/no-v-html': 'off', // Beware of XSS! 52 | 'no-param-reassign': 'off', 53 | 'global-require': 'off', 54 | 'no-unused-vars': 'warn', 55 | 56 | 'import/first': 'off', 57 | 'import/named': 'error', 58 | 'import/namespace': 'error', 59 | 'import/default': 'error', 60 | 'import/export': 'error', 61 | 'import/extensions': 'off', 62 | 'import/no-unresolved': 'off', 63 | 'import/no-extraneous-dependencies': 'off', 64 | 'import/prefer-default-export': 'off', 65 | 66 | // allow console during development only 67 | 'no-console': process.env.NODE_ENV === 'production' ? ['error', { allow: ['warn', 'error'] }] : 'off', 68 | // allow debugger during development only 69 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 70 | }, 71 | }; 72 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: carleslc 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .thumbs.db 3 | node_modules 4 | 5 | # Quasar core related directories 6 | .quasar 7 | # /dist 8 | 9 | # Cordova related directories and files 10 | /src-cordova/node_modules 11 | /src-cordova/platforms 12 | /src-cordova/plugins 13 | /src-cordova/www 14 | 15 | # Capacitor related directories and files 16 | /src-capacitor/www 17 | /src-capacitor/node_modules 18 | 19 | # BEX related directories and files 20 | /src-bex/www 21 | /src-bex/js/core 22 | 23 | # Log files 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # Editor directories and files 29 | .idea 30 | *.suo 31 | *.ntvs* 32 | *.njsproj 33 | *.sln 34 | .vscode 35 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/gallium 2 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | plugins: [ 5 | // to edit target browsers: use "browserslist" field in package.json 6 | require('autoprefixer') 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | index.js -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MyAnime 2 | 3 | Watch your favourite animes with your usual provider, synchronized with [MyAnimeList](https://myanimelist.net/). 4 | 5 | [![ko-fi](https://www.ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/carleslc) 6 | 7 | ![MyAnime](https://i.imgur.com/OnxMvdY.png) 8 | 9 | [![Netlify Status](https://api.netlify.com/api/v1/badges/4ce00d86-1734-45bf-a867-a89d351e45a8/deploy-status)](https://app.netlify.com/sites/my-anime/deploys) 10 | 11 | ## Why to use? 12 | 13 | This is a shortcut to access your favourite anime series with your preferred providers, all in sync with the selected animes in your MyAnimeList profile. 14 | 15 | Avoid surfing the internet or the providers looking for your episodes, just seat in and enjoy. 16 | 17 | With this single page you can test several providers and update your MyAnimeList episodes easily. 18 | 19 | It also skips some advertising from providers home sites, because you'll access to the episode page directly. 20 | 21 | ## How to use 22 | 23 | Enter your MyAnimeList username _(https://myanimelist.net/profile/USERNAME)_, select your preferred provider and choose one of your animes to watch, then just enjoy your episode. 24 | 25 | Click on "Next episode" at the top right corner of an episode to mark it as watched in your MyAnimeList profile. 26 | 27 | When updating your watched episodes in MyAnimeList then the next episode will be shown, waiting for you to be watched. 28 | 29 | ## Features 30 | 31 | - Cover images and links for the series in which you are interested. 32 | - Easy access to your next episodes of MyAnimeList lists Watching, On Hold and Plan to Watch, with several providers for streaming to choose. 33 | - Filter episodes by status (Already aired, Not yet aired), type (TV, Movie, OVA, Special, ONA, Music) and genre. 34 | - Some series have title synonyms, choose between original or alternative title in the settings of the anime. 35 | - Update your watched episodes in your MyAnimeList profile directly within this page. 36 | 37 | Your preferences are saved in the browser, so you only need to change them when you need it. User is automatically retrieved based on the last user used. 38 | 39 | ## Supported providers 40 | 41 | - [MyAnimeList](https://myanimelist.net/) 42 | - [Crunchyroll](https://www.crunchyroll.com/) 43 | - [Netflix](https://www.netflix.com/) 44 | - [Google](https://www.google.com/) 45 | - 🇬🇧 [9Anime](https://9anime.to/) 46 | - 🇬🇧 [Zoro](https://zoro.to/) 47 | - 🇬🇧 [YugenAnime](https://yugen.to/) 48 | - 🇬🇧 [GogoAnime](https://gogoanime.bid/) 49 | - 🇬🇧 [GogoAnime.page](https://gogoanime.page/) 50 | - 🇬🇧 [AnimeSuge](https://animesuge.to/) 51 | - 🇬🇧 [animepahe](https://animepahe.ru/) 52 | - 🇬🇧 [AnimeHeaven](https://animeheaven.ru/) 53 | - 🇬🇧 [Animeflix](https://animeflix.live/) 54 | - 🇬🇧 I'm Feeling Lucky 55 | - 🇪🇸 Voy a tener suerte 56 | - 🇪🇸 [AnimeFlv](https://www.animeflv.net) 57 | - 🇪🇸 [AnimeID](https://www.animeid.tv/) 58 | - 🇪🇸 [AnimeFenix](https://animefenix.tv/) 59 | - 🇪🇸 [jkanime](http://jkanime.net/) 60 | - 🇪🇸 [MonosChinos](https://monoschinos2.com/) 61 | - 🇪🇸 [AnimeMovil2](https://animemovil2.com/) 62 | 63 | Some options are based on search engine (_[DuckDuckGo](https://duckduckgo.com/) I'm Feeling Ducky_), trying to get a proper streamer, but it doesn't mean it always work. Sometimes it redirects to another anime, a non-related page, or it is in another language. 64 | 65 | If selected provider cannot find an episode try to change the provider or choose an alternative title. 66 | 67 | ## Contact 68 | 69 | _Your favourite provider is not listed here? Please, open an issue [here](http://github.com/Carleslc/MyAnime/issues) and we will add it!_ 70 | 71 | Open an issue too if you have any doubt, advice or you want to report about something broken. 72 | 73 | ### Disclaimer 74 | 75 | This website does not host any video, it is a client-side website, just linking and sharing content from non-affiliated external providers. 76 | 77 | Official providers like Crunchyroll or Netflix are recommended. Use other providers at your own risk. 78 | 79 |

80 |
81 | 82 |

83 | 84 |

85 | Enjoy! 86 |

87 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@quasar/babel-preset-app'], 3 | }; 4 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /backend/calendar.js: -------------------------------------------------------------------------------- 1 | const { DateTime } = require('luxon'); 2 | const cheerio = require('cheerio'); 3 | const jsonframe = require('jsonframe-cheerio'); 4 | const isEqual = require('lodash.isequal'); 5 | const get = require('./http-utils'); 6 | const handler = require('./error-handler'); 7 | 8 | const tag = 'calendar'; 9 | const refreshSeconds = 60 * 60 * 24; 10 | const backupTTL = refreshSeconds; 11 | 12 | // Alternative: https://www.livechart.me/timetable 13 | const CALENDAR_URL = 'https://notify.moe/calendar'; 14 | 15 | function formatDateTime(dateTime) { 16 | // https://moment.github.io/luxon/docs/manual/formatting.html#table-of-tokens 17 | return dateTime.toFormat('EEE dd/MM/yyyy, HH:mm ZZZZ'); 18 | } 19 | 20 | function parse(html) { 21 | const _ = cheerio.load(html); 22 | jsonframe(_); 23 | 24 | const frame = { 25 | airingAnimes: { 26 | _s: '.calendar-entry', 27 | _d: [ 28 | { 29 | title: '.calendar-entry-title', 30 | episode: '.calendar-entry-episode | number', 31 | date: '.calendar-entry-time @ datetime', 32 | }, 33 | ], 34 | }, 35 | }; 36 | 37 | /* 38 | airingAnimes: [ 39 | { 40 | title: "One Piece", 41 | episode: "835", 42 | date: "2018-05-06T00:30:00Z" 43 | }, 44 | ... 45 | ] 46 | */ 47 | 48 | return _('.week').scrape(frame); 49 | } 50 | 51 | function set(cache, calendar) { 52 | // eslint-disable-next-line no-unused-vars 53 | cache.add(tag, calendar, { expire: backupTTL, type: 'json' }, (error, _added) => { 54 | if (error) { 55 | console.error(`Calendar set cache error: ${error.message}`); 56 | } else { 57 | console.log('Calendar refreshed successfully'); 58 | } 59 | }); 60 | } 61 | 62 | function removeCalendarDuplicates(calendar) { 63 | const titleEpisodes = {}; // title: [int(episode)] 64 | const filteredEntries = []; 65 | 66 | calendar.airingAnimes.forEach((entry) => { 67 | const sameTitleEpisodes = titleEpisodes[entry.title]; 68 | const entryEpisode = parseInt(entry.episode, 10); 69 | 70 | if (sameTitleEpisodes) { 71 | // Discard duplicated episodes (keep older date, i.e. first episode added) [previousEpisode === entryEpisode] 72 | // Discard older episodes published later (e.g. dubbed versions) [previousEpisode > entryEpisode] 73 | if (!sameTitleEpisodes.some((previousEpisode) => previousEpisode >= entryEpisode)) { 74 | filteredEntries.push(entry); 75 | sameTitleEpisodes.push(entryEpisode); 76 | } 77 | } else { 78 | filteredEntries.push(entry); 79 | titleEpisodes[entry.title] = [entryEpisode]; 80 | } 81 | }); 82 | 83 | return { 84 | airingAnimes: filteredEntries 85 | }; 86 | } 87 | 88 | function clean(calendar) { 89 | try { 90 | return removeCalendarDuplicates(calendar); 91 | } catch (e) { 92 | console.error('Cannot clean calendar', e); 93 | return calendar; 94 | } 95 | } 96 | 97 | function getCurrentCalendar(cache) { 98 | return new Promise((resolve, reject) => { 99 | cache.get(tag, (error, entries) => { 100 | if (error) { 101 | console.error(`Calendar get cache error: ${error.message}`); 102 | reject(error); 103 | } else if (entries.length > 0) { 104 | resolve(entries[0].body); 105 | } else { 106 | console.warn('Calendar not cached!'); 107 | resolve(null); 108 | } 109 | }); 110 | }); 111 | } 112 | 113 | function fetch(cache) { 114 | return new Promise((resolve, reject) => { 115 | function retrieve() { 116 | get(CALENDAR_URL) 117 | .then((html) => { 118 | const calendar = clean(parse(html)); 119 | console.log('Calendar retrieved'); 120 | const calendarData = JSON.stringify(calendar); 121 | if (cache) { 122 | set(cache, calendarData); 123 | } 124 | resolve(calendarData); 125 | }) 126 | .catch(reject); 127 | } 128 | if (cache) { 129 | getCurrentCalendar(cache) 130 | .then((calendarData) => { 131 | if (calendarData !== null) { 132 | resolve(calendarData); 133 | } else { 134 | retrieve(); 135 | } 136 | }) 137 | .catch(reject); 138 | } else retrieve(); 139 | }); 140 | } 141 | 142 | function expire(cache) { 143 | return new Promise((resolve, reject) => { 144 | cache.del(tag, (error, n) => { 145 | if (error) { 146 | console.error(`Calendar del cache error: ${error.message}`); 147 | reject(error); 148 | } else if (n > 0) { 149 | console.log('Expired calendar entries'); 150 | } 151 | resolve(); 152 | }); 153 | }); 154 | } 155 | 156 | function refreshTask(cache) { 157 | function refresh() { 158 | console.log(`[${formatDateTime(DateTime.utc())}] Refreshing calendar...`); 159 | fetch() 160 | .then((newCalendar) => { 161 | getCurrentCalendar(cache).then((oldCalendar) => { 162 | if (oldCalendar === null || !isEqual(oldCalendar, newCalendar)) { 163 | expire(cache).then(() => { 164 | set(cache, newCalendar); 165 | }); 166 | } else { 167 | console.log('Calendar is already up to date'); 168 | } 169 | }); 170 | }) 171 | .catch(handler.httpConsole()); 172 | } 173 | setImmediate(refresh); 174 | const now = DateTime.utc(); 175 | const scheduleLoop = now.startOf('day').plus({ days: 1, hours: 1, minutes: 1 }); // some delay to wait for an update at notify.moe/calendar 176 | const scheduleLoopMillis = scheduleLoop.diff(now).toObject().milliseconds; 177 | console.log( 178 | `Refresh loop will start in ${(scheduleLoopMillis / 1000 / 3600).toFixed(2)} hours at ${formatDateTime( 179 | scheduleLoop 180 | )}` 181 | ); 182 | setTimeout(function scheduleRefresh() { 183 | refresh(); 184 | setInterval(refresh, refreshSeconds * 1000); 185 | }, scheduleLoopMillis); 186 | } 187 | 188 | module.exports = { 189 | fetch, 190 | expire, 191 | refreshTask, 192 | }; 193 | -------------------------------------------------------------------------------- /backend/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | my-anime: 5 | image: node:alpine 6 | container_name: my-anime 7 | restart: unless-stopped 8 | volumes: 9 | - ./:/var/www/app 10 | ports: 11 | - 3000:3000 12 | environment: 13 | - WHITELIST=my-anime.netlify.app,anime.carleslc.me,carleslc.me 14 | - REDIS_HOST=my-anime-redis-cache 15 | - NODE_ENV=production 16 | - PORT=3000 17 | links: 18 | - redis 19 | working_dir: /var/www/app 20 | command: sh -c 'npm i && node server.js' 21 | redis: 22 | image: redis 23 | container_name: my-anime-redis-cache 24 | expose: 25 | - 6379 26 | -------------------------------------------------------------------------------- /backend/error-handler.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | http(res) { 3 | return (err, status) => { 4 | console.error((err.response && err.response.data) || err); 5 | res.status(status || 500).send(err.message || ''); 6 | }; 7 | }, 8 | httpConsole() { 9 | return (err, status) => console.error(`Error ${err}, status: ${status}`); 10 | }, 11 | console() { 12 | return (err) => console.error(err.message); 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /backend/http-utils.js: -------------------------------------------------------------------------------- 1 | const fetch = require('request'); 2 | 3 | module.exports = function get(url, username, password) { 4 | return new Promise((resolve, reject) => { 5 | const options = { url }; 6 | if (username && password) { 7 | options.auth = { 8 | user: username, 9 | password, 10 | }; 11 | } 12 | fetch(options, (error, res, body) => { 13 | if (!error && res.statusCode === 200) { 14 | resolve(body); 15 | } else { 16 | reject(body || error, (res && res.statusCode) || 500); 17 | } 18 | }); 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-anime", 3 | "version": "1.0.0", 4 | "description": "MyAnime API", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/Carleslc/MyAnime.git" 12 | }, 13 | "author": "Carlos Lázaro Costa", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/Carleslc/MyAnime/issues" 17 | }, 18 | "homepage": "https://github.com/Carleslc/MyAnime#readme", 19 | "dependencies": { 20 | "axios": "^0.21.2", 21 | "basic-auth": "^2.0.0", 22 | "body-parser": "^1.20.1", 23 | "cheerio": "^1.0.0-rc.2", 24 | "cors": "^2.8.4", 25 | "express": "^4.18.2", 26 | "express-redis-cache": "^1.1.1", 27 | "full-icu": "^1.2.1", 28 | "jsonframe-cheerio": "^3.0.1", 29 | "lodash.isequal": "^4.5.0", 30 | "luxon": "^1.28.1", 31 | "nconf": "^0.11.4", 32 | "popura": "^1.2.5", 33 | "redis": "^3.1.1", 34 | "request": "^2.86.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/providers/Aniwatch.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const { generateToken } = require('../utils/strings'); 3 | 4 | // This provider is no longer active 5 | // We keep this file only as a reference 6 | 7 | class Aniwatch { 8 | constructor() { 9 | this.url = 'https://aniwatch.me/'; 10 | 11 | this.axios = axios.create({ 12 | baseURL: `${this.url}api/ajax/APIHandle`, 13 | headers: { 14 | common: { 15 | Accept: 'application/json', 16 | 'Content-Type': 'application/json', 17 | 'User-Agent': 18 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36', 19 | }, 20 | }, 21 | }); 22 | 23 | this.api = { 24 | post: (data, opts) => this.axios.post('', JSON.stringify(data), opts), 25 | }; 26 | } 27 | 28 | buildUrl(id, episode) { 29 | return id ? `${this.url}anime/${id}/${episode || ''}` : null; 30 | } 31 | 32 | async search(title) { 33 | console.log('Aniwatch search', title); 34 | const xsrf = generateToken(); 35 | const response = await this.api.post( 36 | { 37 | controller: 'Search', 38 | action: 'search', 39 | rOrder: false, 40 | order: 'title', 41 | typed: title, 42 | }, 43 | { 44 | withCredentials: true, 45 | headers: { 46 | 'x-path': '/search', 47 | 'x-xsrf-token': xsrf, 48 | referer: this.url, 49 | Cookie: `XSRF-TOKEN=${xsrf};`, 50 | }, 51 | } 52 | ); 53 | let id = null; 54 | if (response.data.length > 0) { 55 | id = response.data[0].detail_id || null; 56 | } 57 | return id; 58 | } 59 | 60 | async getUrl(cache, title, episode) { 61 | let id = await cache.get(title); 62 | 63 | if (id === undefined) { 64 | id = await this.search(title); 65 | cache.save(title, id); 66 | } 67 | 68 | return this.buildUrl(id, episode); 69 | } 70 | } 71 | 72 | module.exports = new Aniwatch(); 73 | -------------------------------------------------------------------------------- /backend/providers/providers.js: -------------------------------------------------------------------------------- 1 | const expiration = 60 * 60 * 24; // 1 day in seconds 2 | 3 | const providers = { 4 | Aniwatch: require('./Aniwatch'), 5 | }; 6 | 7 | function expire(cache, provider) { 8 | return new Promise((resolve, reject) => { 9 | cache.del(`${provider}.*`, (error, n) => { 10 | if (error) { 11 | console.error(`${provider} expire cache error: ${error.message}`); 12 | reject(error); 13 | } else if (n > 0) { 14 | console.log(`Expired ${n} ${provider} entries`); 15 | } 16 | resolve(); 17 | }); 18 | }); 19 | } 20 | 21 | function tag(provider, key) { 22 | return `${provider}.${key}`; 23 | } 24 | 25 | function providerCache(cache, provider) { 26 | return { 27 | get: (key) => 28 | new Promise((resolve) => { 29 | cache.get(tag(provider, key), (error, entries) => { 30 | if (error) { 31 | console.error(`${provider} cache error: ${error.message}`); 32 | } else if (entries.length > 0) { 33 | resolve(JSON.parse(entries[0].body)); 34 | } else { 35 | resolve(); 36 | } 37 | }); 38 | }), 39 | save: (key, value) => { 40 | cache.add(tag(provider, key), JSON.stringify(value), { expire: expiration }, (error) => { 41 | if (error) { 42 | console.error(`Provider set cache error: ${error.message}`); 43 | } 44 | }); 45 | }, 46 | }; 47 | } 48 | 49 | module.exports = { 50 | has(provider) { 51 | return !!provider && !!providers[provider]; 52 | }, 53 | getUrl(cache, provider, title, episode) { 54 | return providers[provider].getUrl(providerCache(cache, provider), title, episode); 55 | }, 56 | expireAll(cache) { 57 | Object.keys(providers).forEach((provider) => expire(cache, provider)); 58 | }, 59 | }; 60 | -------------------------------------------------------------------------------- /backend/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const app = express(); 4 | 5 | const cors = require('cors'); 6 | 7 | app.use(cors()); 8 | 9 | const nconf = require('nconf'); 10 | 11 | nconf.argv().env(); 12 | 13 | const port = nconf.get('PORT') || 3000; 14 | 15 | // eslint-disable-next-line import/order 16 | const handler = require('./error-handler'); 17 | 18 | const redis = require('redis'); 19 | 20 | const client = redis 21 | .createClient(nconf.get('REDIS_PORT') || '6379', nconf.get('REDIS_HOST') || '127.0.0.1', { 22 | auth_pass: nconf.get('REDIS_KEY'), 23 | return_buffers: true, 24 | }) 25 | .on('error', handler.console()); 26 | 27 | const cache = require('express-redis-cache')({ client, prefix: 'anime' }); 28 | 29 | /* cache.on('message', function(message) { 30 | console.log(message); 31 | }); */ 32 | 33 | app.get('/', (req, res) => { 34 | res.send('

MyAnime API

Go to the app'); 35 | }); 36 | 37 | const calendar = require('./calendar'); 38 | 39 | app.get('/calendar', (req, res) => { 40 | res.setHeader('Content-Type', 'application/json'); 41 | calendar 42 | .fetch(cache) 43 | .then((cal) => res.send(cal)) 44 | .catch(handler.http(res)); 45 | }); 46 | 47 | const providers = require('./providers/providers'); 48 | 49 | let whitelist = []; 50 | 51 | const whitelistEnv = nconf.get('WHITELIST'); 52 | if (whitelistEnv) { 53 | whitelist = whitelistEnv.split(','); 54 | } 55 | 56 | app.get('/provider/:name', (req, res) => { 57 | const origin = req.headers.origin || req.headers.host; 58 | if (whitelist.length > 0 && !whitelist.includes(origin)) { 59 | res.status(403).send(`Forbidden (whitelist). Origin ${origin} not allowed.`); 60 | return; 61 | } 62 | 63 | const provider = req.params.name; 64 | const title = req.query.title; 65 | const episode = req.query.episode; 66 | 67 | if (!providers.has(provider)) { 68 | res.status(404).send(`Provider ${provider} not found`); 69 | } else if (!title) { 70 | res.status(400).send('Missing title parameter'); 71 | } else { 72 | providers 73 | .getUrl(cache, provider, title, episode) 74 | .then((url) => { 75 | if (url) { 76 | res.send({ url }); 77 | } else { 78 | res.status(404).send(`Anime ${title} not found for provider ${provider}`); 79 | } 80 | }) 81 | .catch(handler.http(res)); 82 | } 83 | }); 84 | 85 | providers.expireAll(cache); 86 | 87 | calendar.refreshTask(cache); 88 | 89 | app.listen(port, (err) => { 90 | console.log(err || `Server is listening on ${port}`); 91 | }); 92 | -------------------------------------------------------------------------------- /backend/utils/strings.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | generateToken(n = 32, chars = ['a', 'b', 'c', 'd', 'e', 'f', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']) { 3 | let result = ''; 4 | // eslint-disable-next-line no-plusplus 5 | for (let i = 0; i < n; i++) { 6 | result += chars[Math.floor(Math.random() * chars.length)]; 7 | } 8 | return result; 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /dist/spa/chibi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/dist/spa/chibi.png -------------------------------------------------------------------------------- /dist/spa/css/2.bc3cfb91.css: -------------------------------------------------------------------------------- 1 | .about a{color:#e242bd;text-decoration:none}.about a:active,.about a:hover{text-decoration:underline}.about ul{margin-top:0}.about ul a{color:#5e3175}.about ul a:active,.about ul a:hover{color:#e242bd;text-decoration:none}.about .section p:last-child{margin-bottom:0} -------------------------------------------------------------------------------- /dist/spa/css/3.cbb1f13c.css: -------------------------------------------------------------------------------- 1 | .icon-only{border-radius:3px}.icon-only .q-item__section{padding:0;min-width:0} -------------------------------------------------------------------------------- /dist/spa/css/4.f4c9749e.css: -------------------------------------------------------------------------------- 1 | .q-icon>img{width:2rem;height:2rem} -------------------------------------------------------------------------------- /dist/spa/css/app.aa8f7688.css: -------------------------------------------------------------------------------- 1 | body{background-color:#2d1e53}body *{font-size:1rem}.q-footer,.q-page{background:transparent}.q-footer{width:-moz-fit-content;width:fit-content;left:auto}.q-header{background-color:#2d1e53;height:136px;z-index:5}.q-header .q-toolbar{height:88px}.gradient{background-image:linear-gradient(#2d1e53,#e242bd);background-color:#2d1e53}.q-page-container{padding-bottom:0!important}.q-btn{color:#fff}@media (max-width:349px){#settings{min-width:30px}.gt-xxs{display:none}.q-drawer__opener{width:4px}}.q-drawer{max-width:100vw}.q-drawer .q-item__section{margin:0}.q-tooltip{max-width:256px;text-align:center;font-size:1em;background-color:#1d1d1d;z-index:4000}.q-tooltip>div,.q-tooltip>p{font-size:1.05em}.prefix{font-size:16px;padding-bottom:2px}.user-search{min-width:192px;max-width:25vw}.user-search .q-field__control{padding-right:0}.user-search.q-field--focused .q-btn{color:#fff;background-color:#068b25}.user-search.q-field--focused .q-btn--flat{color:#1d1d1d;background-color:transparent}@media (max-width:349px){.user-search{min-width:152px}.user-search .q-btn{width:40px}}.q-select .q-field__before,.q-select .q-field__prepend{padding-right:12px}.q-select .q-field__after,.q-select .q-field__append{padding-left:12px}.q-select .q-field__inner{min-width:42px}.q-select.q-field--standout.q-field--focused .filter-options{background:rgba(45,30,83,0.1)}.q-select.q-field--standout.q-field--focused .filter-options .q-field__native{color:#1d1d1d}.q-select.q-field--standout.q-field--focused .filter-options .q-field__marginal{color:rgba(0,0,0,0.54)}.filter-options{color:grey}.filter-options .q-item--active{color:#e242bd;font-weight:700}@media (max-width:750px){.gt-xsm{display:none}}h1{font-size:3em;font-weight:700;margin:0}h2{font-size:1.5em;font-weight:600;margin:0.25em 0}p{text-align:justify}a.link{color:#1d1d1d;text-decoration:none}a.link-underline{color:#2d1e53;text-decoration:underline}a.link:active,a.link:hover{color:#e242bd}.clickable{cursor:pointer}.anime-container{display:grid;grid-gap:10px;grid-template-columns:repeat(auto-fill,calc(12.5% - 8.75px));justify-content:space-between}@media (min-width:2304px){.anime-container{grid-template-columns:repeat(auto-fill,calc(8.33333% - 9.16667px))}}@media (max-width:1439.98px){.anime-container{grid-template-columns:repeat(auto-fill,calc(16.66667% - 8.33333px))}}@media (max-width:1023.98px){.anime-container{grid-template-columns:repeat(auto-fill,calc(25% - 7.5px))}}@media (max-width:599.98px){.anime-container{grid-template-columns:repeat(auto-fill,calc(50% - 5px))}}@media (max-width:288px){.anime-container{grid-template-columns:repeat(auto-fill,100%)}}.anime-container .anime-episode.q-card{min-width:128px;border-radius:12px;overflow:hidden;box-shadow:none;outline:0}.anime-container .anime-episode.q-card h1,.anime-container .anime-episode.q-card h2{vertical-align:middle;text-align:center;color:#fff;text-shadow:0 1px 0 #000;line-height:1.2em;font-family:Roboto,Helvetica,Arial,sans-serif;z-index:1}.anime-container .anime-episode.q-card h1{font-size:1.2em;font-weight:700}.anime-container .anime-episode.q-card.small h1,.anime-container .anime-episode.q-card h2{font-size:1em}.anime-container .anime-episode.q-card.small h2{font-size:0.8em}.anime-container .anime-episode.q-card .q-btn,.anime-container .anime-episode.q-card .q-chip{z-index:2}.anime-container .anime-episode.q-card .q-img{image-rendering:optimizespeed;image-rendering:-moz-crisp-edges;image-rendering:-o-crisp-edges;image-rendering:-webkit-optimize-contrast;-ms-interpolation-mode:nearest-neighbor;min-height:200px}@media (max-width:599.98px){.anime-container .anime-episode.q-card .q-img{min-height:300px}}.overlay:after{content:"";background:rgba(0,0,0,0.6);position:absolute;top:0;right:0;bottom:0;left:0}.on-hover .hoverable{opacity:0;transition:opacity 200ms ease-in-out}.on-hover:focus-within .hoverable,.on-hover:focus .hoverable,.on-hover:hover .hoverable{opacity:1;transition:opacity 200ms ease-in-out}.q-skeleton:before{content:""}.square{border-radius:0}.fit-width{width:100%} -------------------------------------------------------------------------------- /dist/spa/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/dist/spa/favicon.ico -------------------------------------------------------------------------------- /dist/spa/fonts/KFOkCnqEu92Fr1MmgVxIIzQ.68bb21d0.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/dist/spa/fonts/KFOkCnqEu92Fr1MmgVxIIzQ.68bb21d0.woff -------------------------------------------------------------------------------- /dist/spa/fonts/KFOlCnqEu92Fr1MmEU9fBBc-.48af7707.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/dist/spa/fonts/KFOlCnqEu92Fr1MmEU9fBBc-.48af7707.woff -------------------------------------------------------------------------------- /dist/spa/fonts/KFOlCnqEu92Fr1MmSU5fBBc-.c2f7ab22.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/dist/spa/fonts/KFOlCnqEu92Fr1MmSU5fBBc-.c2f7ab22.woff -------------------------------------------------------------------------------- /dist/spa/fonts/KFOlCnqEu92Fr1MmWUlfBBc-.77ecb942.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/dist/spa/fonts/KFOlCnqEu92Fr1MmWUlfBBc-.77ecb942.woff -------------------------------------------------------------------------------- /dist/spa/fonts/KFOlCnqEu92Fr1MmYUtfBBc-.f5677eb2.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/dist/spa/fonts/KFOlCnqEu92Fr1MmYUtfBBc-.f5677eb2.woff -------------------------------------------------------------------------------- /dist/spa/fonts/KFOmCnqEu92Fr1Mu4mxM.f1e2a767.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/dist/spa/fonts/KFOmCnqEu92Fr1Mu4mxM.f1e2a767.woff -------------------------------------------------------------------------------- /dist/spa/fonts/fa-brands-400.2285773e.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/dist/spa/fonts/fa-brands-400.2285773e.woff -------------------------------------------------------------------------------- /dist/spa/fonts/fa-brands-400.d878b0a6.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/dist/spa/fonts/fa-brands-400.d878b0a6.woff2 -------------------------------------------------------------------------------- /dist/spa/fonts/fa-regular-400.7a333762.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/dist/spa/fonts/fa-regular-400.7a333762.woff2 -------------------------------------------------------------------------------- /dist/spa/fonts/fa-regular-400.bb58e57c.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/dist/spa/fonts/fa-regular-400.bb58e57c.woff -------------------------------------------------------------------------------- /dist/spa/fonts/fa-solid-900.1551f4f6.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/dist/spa/fonts/fa-solid-900.1551f4f6.woff2 -------------------------------------------------------------------------------- /dist/spa/fonts/fa-solid-900.eeccf4f6.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/dist/spa/fonts/fa-solid-900.eeccf4f6.woff -------------------------------------------------------------------------------- /dist/spa/fonts/flUhRq6tzZclQEJ-Vdg-IuiaDsNa.4d73cb90.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/dist/spa/fonts/flUhRq6tzZclQEJ-Vdg-IuiaDsNa.4d73cb90.woff -------------------------------------------------------------------------------- /dist/spa/fonts/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.0383092b.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/dist/spa/fonts/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.0383092b.woff2 -------------------------------------------------------------------------------- /dist/spa/icons/animeflv.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/dist/spa/icons/animeflv.ico -------------------------------------------------------------------------------- /dist/spa/icons/animepahe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/dist/spa/icons/animepahe.png -------------------------------------------------------------------------------- /dist/spa/icons/crunchyroll.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/dist/spa/icons/crunchyroll.png -------------------------------------------------------------------------------- /dist/spa/icons/favicon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/dist/spa/icons/favicon-128x128.png -------------------------------------------------------------------------------- /dist/spa/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/dist/spa/icons/favicon-16x16.png -------------------------------------------------------------------------------- /dist/spa/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/dist/spa/icons/favicon-32x32.png -------------------------------------------------------------------------------- /dist/spa/icons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/dist/spa/icons/favicon-96x96.png -------------------------------------------------------------------------------- /dist/spa/icons/monoschinos.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/dist/spa/icons/monoschinos.ico -------------------------------------------------------------------------------- /dist/spa/icons/youtube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/dist/spa/icons/youtube.png -------------------------------------------------------------------------------- /dist/spa/index.html: -------------------------------------------------------------------------------- 1 | MyAnime
-------------------------------------------------------------------------------- /dist/spa/js/10.6910009d.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([[10],{"172f":function(e,t,i){"use strict";i.r(t);var s=function(){var e=this,t=e._self._c;return t("q-card",[t("q-card-section",{staticClass:"row items-center no-wrap q-py-sm"},[t("a",{staticClass:"link text-center text-h6",attrs:{href:e.animeUrl,target:"_blank"}},[e._v(e._s(e.title))]),t("q-space"),t("q-btn",{directives:[{name:"close-popup",rawName:"v-close-popup"}],staticClass:"q-ml-md",attrs:{icon:"close",flat:"",round:"",dense:"",color:"grey"}})],1),t("q-separator"),t("q-card-section",[t("q-list",{staticClass:"column"},[t("div",{staticClass:"row"},[t("q-item-section",[t("q-item-label",{directives:[{name:"t",rawName:"v-t",value:"selectProvider",expression:"'selectProvider'"}],staticClass:"q-pt-none q-px-sm",attrs:{header:""}}),t("provider-select",{attrs:{value:e.provider,"options-dense":"",standout:"filter-options",tooltip:!1},on:{input:e.updateProvider}})],1)],1),e.anime.titles.length>1?t("div",{staticClass:"row"},[t("q-item-section",[t("q-item-label",{directives:[{name:"t",rawName:"v-t",value:"selectTitle",expression:"'selectTitle'"}],staticClass:"q-px-sm",attrs:{header:""}}),t("title-select",{attrs:{value:e.title,titles:e.anime.titles},on:{input:e.updateTitle}})],1)],1):e._e()])],1),t("q-separator")],1)},a=[],r=i("ded3"),n=i.n(r),l=i("2f62"),o={props:{anime:{type:Object,required:!0}},computed:n()(n()(n()({},Object(l["e"])("store",{animeUrl:function(e){var t=e.api;return t.animeUrl(this.anime)}})),Object(l["c"])("store",["providerByAnimeTitle","titleByAnimeId"])),{},{provider:function(){return this.providerByAnimeTitle(this.title)},title:function(){return this.titleByAnimeId(this.anime.id)||this.anime.title}}),methods:n()(n()({},Object(l["d"])("store",["setProviderByTitle","setAlternativeTitle"])),{},{updateProvider:function(e){this.setProviderByTitle({title:this.title,provider:e})},updateTitle:function(e){this.setAlternativeTitle({anime:this.anime,title:e})}})},c=o,p=i("2877"),d=i("f09f"),m=i("a370"),u=i("2c91"),v=i("9c40"),h=i("eb85"),q=i("1c1c"),f=i("4074"),b=i("0170"),w=i("7f67"),C=i("eebe"),T=i.n(C),y=Object(p["a"])(c,s,a,!1,null,null,null);t["default"]=y.exports;T()(y,"components",{QCard:d["a"],QCardSection:m["a"],QSpace:u["a"],QBtn:v["a"],QSeparator:h["a"],QList:q["a"],QItemSection:f["a"],QItemLabel:b["a"]}),T()(y,"directives",{ClosePopup:w["a"]})}}]); -------------------------------------------------------------------------------- /dist/spa/js/11.d74a3687.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([[11],{df3a:function(e,t,a){"use strict";a.r(t);a("b0c0");var r=function(){var e=this,t=e._self._c;return e.icon?t("a",{attrs:{href:e.url,target:"_blank"}},[e.icon?t("q-avatar",{attrs:{size:e.size}},[t("img",{attrs:{src:e.picture}})]):e._e()],1):t("q-item",{directives:[{name:"ripple",rawName:"v-ripple"}],attrs:{tag:"a",href:e.url,target:"_blank"}},[t("q-item-section",{attrs:{avatar:""}},[t("q-avatar",{attrs:{size:e.size}},[t("img",{attrs:{src:e.picture}})])],1),t("q-item-section",[t("q-item-label",{staticClass:"text-h5"},[e._v(e._s(e.username||e.api.name))])],1)],1)},i=[],s=a("ded3"),n=a.n(s),c=a("2f62"),l={props:{size:{type:String,default:"56px"},icon:{type:Boolean,default:!1}},computed:n()(n()({},Object(c["e"])("store",["api","username","picture"])),{},{url:function(){return this.api.getUserProfileUrl(this.username)}})},p=l,u=a("2877"),o=a("cb32"),m=a("66e5"),f=a("4074"),b=a("0170"),d=a("714f"),v=a("eebe"),g=a.n(v),h=Object(u["a"])(p,r,i,!1,null,null,null);t["default"]=h.exports;g()(h,"components",{QAvatar:o["a"],QItem:m["a"],QItemSection:f["a"],QItemLabel:b["a"]}),g()(h,"directives",{Ripple:d["a"]})}}]); -------------------------------------------------------------------------------- /dist/spa/js/12.92284c22.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([[12],{"66b7":function(n,t,o){"use strict";o.r(t);var e=function(){var n=this,t=n._self._c;return n.home?n._e():t("q-btn",{attrs:{round:"",flat:"",icon:"arrow_back"},on:{click:function(t){return n.back()}}})},c=[],r={name:"Back",computed:{home:function(){return"/"===this.$route.path}},methods:{back:function(){this.$router.go(-1)}}},u=r,a=o("2877"),i=o("9c40"),s=o("eebe"),l=o.n(s),b=Object(a["a"])(u,e,c,!1,null,null,null);t["default"]=b.exports;l()(b,"components",{QBtn:i["a"]})}}]); -------------------------------------------------------------------------------- /dist/spa/js/13.4d6a33f0.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([[13],{"836a":function(n,t,a){"use strict";a.r(t);var e=function(){var n=this,t=n._self._c;return t("item-button",{attrs:{icon:"date_range",href:"https://notify.moe/calendar",target:"_blank",label:n.icon?"":n.$t("animeCalendar"),caption:n.$t("animeCalendarDescription")}})},o=[],i={props:{icon:{type:Boolean,default:!1}}},l=i,r=a("2877"),c=Object(r["a"])(l,e,o,!1,null,null,null);t["default"]=c.exports}}]); -------------------------------------------------------------------------------- /dist/spa/js/14.a0442d14.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([[14],{"7a1a":function(e,n,t){"use strict";t.r(n);var a=function(){var e=this,n=e._self._c;return e.open?n("div",{staticClass:"clickable non-selectable",attrs:{"aria-label":e.aria},on:{click:e.openOnce}},[e._t("default")],2):n("a",{staticClass:"non-selectable",attrs:{href:e.href,target:"_blank","aria-label":e.aria},on:{mousedown:function(e){e.preventDefault()}}},[e._t("default")],2)},i=[],r=t("7ec2"),s=t.n(r),c=t("c973"),l=t.n(c),o={props:{href:{type:String,default:"#"},open:{type:Function,default:void 0},aria:{type:String,default:void 0}},data:function(){return{isOpening:!1}},methods:{openOnce:function(){var e=this;return l()(s()().mark((function n(){return s()().wrap((function(n){while(1)switch(n.prev=n.next){case 0:if(e.isOpening){n.next=5;break}return e.isOpening=!0,n.next=4,e.open();case 4:e.isOpening=!1;case 5:case"end":return n.stop()}}),n)})))()}}},u=o,p=t("2877"),f=Object(p["a"])(u,a,i,!1,null,null,null);n["default"]=f.exports}}]); -------------------------------------------------------------------------------- /dist/spa/js/15.601bcce8.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([[15],{b8c0:function(e,n,t){"use strict";t.r(n);var a=function(){var e=this,n=e._self._c;return n("q-select",{ref:"languageSelect",attrs:{value:e.language,dark:"",dense:"",standout:"","options-selected-class":"filter-options","options-dark":!1,options:e.languages},on:{input:function(n){return e.selectLanguage(n)},"popup-show":function(n){return e.focus()},"popup-hide":function(n){return e.focus(!1)}},scopedSlots:e._u([{key:"prepend",fn:function(){return[n("q-icon",{attrs:{name:"language"}})]},proxy:!0},{key:"option",fn:function(t){return[n("q-item",e._g(e._b({directives:[{name:"ripple",rawName:"v-ripple"}]},"q-item",t.itemProps,!1),t.itemEvents),[n("q-item-section",{attrs:{avatar:""}},[e._v("\n "+e._s(t.opt.icon)+"\n ")]),n("q-item-section",[n("q-item-label",[e._v(e._s(t.opt.label))])],1)],1)]}}])})},s=[],i=(t("7db0"),t("d3b7"),t("4e82"),t("8847")),o={props:{value:{type:String,default:"en"}},data:function(){return{language:null,languages:[{label:"English",value:"en",icon:Object(i["b"])("en")},{label:"Español",value:"es",icon:Object(i["b"])("es")}]}},created:function(){var e=this;this.language=this.languages.find((function(n){return n.value===e.value}))||this.languages[0],this.sortLanguageOptions()},methods:{sortLanguageOptions:function(){var e=this;this.languages.sort((function(n,t){return n===e.language?-1:t===e.language?1:n.label.localeCompare(t.label)}))},selectLanguage:function(e){this.language=e,this.$emit("input",e.value),this.sortLanguageOptions()},focus:function(){var e=!(arguments.length>0&&void 0!==arguments[0])||arguments[0];this.$refs.languageSelect.focused=e}}},u=o,l=t("2877"),r=t("ddd8"),c=t("0016"),g=t("66e5"),p=t("4074"),f=t("0170"),d=t("714f"),v=t("eebe"),b=t.n(v),h=Object(l["a"])(u,a,s,!1,null,null,null);n["default"]=h.exports;b()(h,"components",{QSelect:r["a"],QIcon:c["a"],QItem:g["a"],QItemSection:p["a"],QItemLabel:f["a"]}),b()(h,"directives",{Ripple:d["a"]})}}]); -------------------------------------------------------------------------------- /dist/spa/js/16.ee88b894.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([[16],{"40ad":function(e,t,s){"use strict";s.r(t);s("b0c0");var a=function(){var e=this,t=e._self._c;return t("q-dialog",{attrs:{persistent:""},model:{value:e.authNeeded,callback:function(t){e.authNeeded=t},expression:"authNeeded"}},[t("q-card",{staticStyle:{"min-width":"300px"}},[t("q-form",{on:{submit:e.setPassword}},[t("q-card-section",{staticClass:"row justify-between items-center"},[t("div",{directives:[{name:"t",rawName:"v-t",value:"login",expression:"'login'"}],staticClass:"text-h5 text-bold"}),t("a",{attrs:{href:e.api.homeUrl,target:"_blank"}},[t("q-avatar",{attrs:{rounded:"",size:"lg"}},[t("img",{attrs:{src:e.api.image}})])],1)]),t("q-card-section",{staticClass:"q-pt-none"},[t("div",[e._v("\n "+e._s(e.$t("loginDescription",{api:e.api.name}))+"\n ")])]),t("q-card-section",{staticClass:"q-pt-none column"},[t("q-input",{attrs:{dense:"",placeholder:e.$t("username"),rules:[function(t){return!e.isBlank(t)||e.$t("required.username")}]},model:{value:e.username,callback:function(t){e.username=t},expression:"username"}}),t("q-input",{attrs:{dense:"",autofocus:"",type:"password",placeholder:e.$t("password"),rules:[function(t){return!e.isBlank(t)||e.$t("required.password")}]},model:{value:e.password,callback:function(t){e.password=t},expression:"password"}})],1),t("q-card-section",{staticClass:"q-py-none text-grey-6"},[t("span",{directives:[{name:"t",rawName:"v-t",value:"notRegisteredYet",expression:"'notRegisteredYet'"}],staticClass:"q-mr-sm"}),t("a",{staticClass:"link link-underline",attrs:{href:e.api.registerUrl,target:"_blank"}},[e._v(e._s(e.$t("registerHere")))])]),e.api.setPasswordUrl?t("i18n",{staticClass:"q-pb-none text-italic text-grey-6",attrs:{path:"noPassword",tag:"q-card-section"}},[t("a",{staticClass:"link link-underline",attrs:{href:e.api.setPasswordUrl,target:"_blank"}},[e._v(e._s(e.$t("accountSettings")))])]):e._e(),t("q-card-actions",{staticClass:"text-primary",attrs:{align:"right"}},[e.isLoading?e._e():t("q-btn",{directives:[{name:"close-popup",rawName:"v-close-popup"}],attrs:{flat:"",color:"grey",label:e.$t("cancel")}}),t("q-btn",{attrs:{flat:"",color:"primary",type:"submit",label:e.$t("login"),disable:!e.isValid,loading:e.isLoading}})],1)],1)],1)],1)},r=[],n=s("ded3"),i=s.n(n),o=(s("d3b7"),s("e6cf"),s("a79d"),s("002d")),c=s("2f62"),l=s("5935"),d={data:function(){return{username:"",password:""}},computed:i()(i()(i()(i()(i()({},Object(c["e"])("store",{currentUsername:function(e){return e.username}})),Object(c["e"])("store",["api"])),Object(l["b"])("store",["authNeeded"])),Object(c["c"])("store",["isLoading"])),{},{isValid:function(){return!Object(o["a"])(this.password)&&!Object(o["a"])(this.username)}}),watch:{currentUsername:function(){this.username=this.currentUsername,this.password=""}},created:function(){this.username=this.currentUsername},methods:i()(i()(i()({},Object(c["b"])("store",["login","searchUser"])),Object(c["d"])("store",["setAuthNeeded"])),{},{isBlank:o["a"],setPassword:function(){var e=this,t=Object(o["d"])(this.username);this.login({username:t,password:this.password}).then((function(){e.searchUser(t)})).finally((function(){e.setAuthNeeded(!1)}))}})},u=d,p=s("2877"),m=s("24e8"),b=s("f09f"),h=s("0378"),f=s("a370"),g=s("cb32"),w=s("27f9"),v=s("4b7e"),q=s("9c40"),k=s("7f67"),C=s("eebe"),x=s.n(C),_=Object(p["a"])(u,a,r,!1,null,null,null);t["default"]=_.exports;x()(_,"components",{QDialog:m["a"],QCard:b["a"],QForm:h["a"],QCardSection:f["a"],QAvatar:g["a"],QInput:w["a"],QCardActions:v["a"],QBtn:q["a"]}),x()(_,"directives",{ClosePopup:k["a"]})}}]); -------------------------------------------------------------------------------- /dist/spa/js/17.0d3a8f1c.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([[17],{"19a6":function(e,t,n){"use strict";n.r(t);var s=function(){var e=this,t=e._self._c;return t("item-button",{attrs:{dense:"",icon:"delete",label:e.$t("resetSettings"),caption:e.$t("resetSettingsDescription")},on:{click:e.reset}})},c=[],r=n("ded3"),i=n.n(r),l=n("2f62"),o={methods:i()(i()({},Object(l["d"])("store",["clear"])),{},{reset:function(){this.$q.localStorage.clear(),this.clear()}})},a=o,u=n("2877"),d=Object(u["a"])(a,s,c,!1,null,null,null);t["default"]=d.exports}}]); -------------------------------------------------------------------------------- /dist/spa/js/18.f8da8327.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([[18],{"6ec6":function(e,t,n){"use strict";n.r(t);var s=function(){var e=this,t=e._self._c;return t("q-form",{on:{submit:e.searchUserInput}},[t("q-input",{staticClass:"user-search",attrs:{dark:"",dense:"",standout:"",placeholder:"Username"},scopedSlots:e._u([{key:"prepend",fn:function(){return[t("span",{staticClass:"prefix"},[e._v("@")]),t("q-tooltip",{attrs:{anchor:"center right",self:"center left","transition-show":"fade","transition-hide":"fade",offset:[0,0]}},[e._v("\n "+e._s(e.api.profileUrl)+"\n ")])]},proxy:!0},{key:"append",fn:function(){return[t("q-btn",{attrs:{flat:e.username===e.input||!e.filled,loading:e.isLoading,disabled:!e.filled,icon:"search",type:"submit"},on:{click:e.searchUserInput}})]},proxy:!0}]),model:{value:e.input,callback:function(t){e.input=t},expression:"input"}}),t("password-dialog")],1)},i=[],r=n("ded3"),a=n.n(r),o=n("002d"),u=n("2f62"),c={data:function(){return{input:""}},computed:a()(a()(a()({},Object(u["e"])("store",["username","api"])),Object(u["c"])("store",["isLoading","hasUsername"])),{},{filled:function(){return!Object(o["a"])(this.input)}}),watch:{username:function(){this.username!==this.input&&(this.input=this.username)}},created:function(){this.hasUsername&&(this.input=this.username,this.searchUserInput())},methods:a()(a()({},Object(u["b"])("store",["searchUser"])),{},{searchUserInput:function(){this.searchUser(Object(o["d"])(this.input))}})},p=c,l=n("2877"),d=n("0378"),h=n("27f9"),f=n("05c0"),m=n("9c40"),b=n("eebe"),U=n.n(b),w=Object(l["a"])(p,s,i,!1,null,null,null);t["default"]=w.exports;U()(w,"components",{QForm:d["a"],QInput:h["a"],QTooltip:f["a"],QBtn:m["a"]})}}]); -------------------------------------------------------------------------------- /dist/spa/js/19.1e9e23d1.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([[19],{"713b":function(t,e,s){"use strict";s.r(e);var a=function(){var t=this,e=t._self._c;return e("q-layout",{attrs:{view:"hHr lpR fFr"}},[e("q-header",[e("q-toolbar",{staticClass:"row justify-between items-center q-pa-md no-wrap scroll",attrs:{color:"purple"}},[e("div",{staticClass:"col-auto row items-center no-wrap",style:t.hiddenIfSettings},[e("back",{staticClass:"q-mr-md"}),e("div",{staticClass:"row justify-between items-center no-wrap"},[e("avatar",{staticClass:"col-shrink q-mr-lg gt-xxs",attrs:{icon:""}}),e("user-search",{staticClass:"col-grow"})],1)],1),t.settings?e("div",{staticClass:"col row justify-around items-center"},[e("div",{staticClass:"col-auto text-h4"},[t._v("My Anime")])]):e("div",{staticClass:"col row justify-end items-center no-wrap",on:{mousedown:function(t){t.preventDefault()}}},[e("provider-select",{ref:"providerSelect",staticClass:"col-auto q-mx-auto gt-xsm",attrs:{value:t.provider,dark:"",icon:""},on:{input:t.setProvider}}),e("div",{staticClass:"col-auto q-gutter-x-lg q-mr-auto row justify-between gt-md"},[e("status-select",{staticClass:"col-auto gt-md",attrs:{icon:"ondemand_video",caption:t.$t("animeStatusFilter"),options:t.config.airingStatuses},model:{value:t.airingStatusFilter,callback:function(e){t.airingStatusFilter=e},expression:"airingStatusFilter"}}),e("status-select",{staticClass:"col-auto gt-md",attrs:{icon:"tv",caption:t.$t("animeTypeFilter"),options:t.config.animeTypes},model:{value:t.typeFilter,callback:function(e){t.typeFilter=e},expression:"typeFilter"}})],1),e("div",{staticClass:"col-auto row justify-end q-gutter-x-md"},[e("calendar-button",{staticClass:"col-shrink gt-xs",attrs:{icon:""}})],1)],1),e("q-btn",{staticClass:"col-shrink q-ml-md q-py-xs",attrs:{id:"settings",flat:"",icon:"settings"},on:{click:function(e){t.settings=!t.settings},keydown:function(e){if(!e.type.indexOf("key")&&t._k(e.keyCode,"esc",27,e.key,["Esc","Escape"]))return null;t.settings=!1}}})],1),e("q-tabs",{staticClass:"text-grey-7",attrs:{"inline-label":"",align:"justify","active-color":"white","indicator-color":"transparent"},model:{value:t.status,callback:function(e){t.status=e},expression:"status"}},t._l(t.config.statuses,(function(s,a){return e("q-tab",{key:a,class:{"q-px-xs":t.$q.screen.lt.sm},attrs:{name:a,icon:s.icon,label:s.label}})})),1)],1),e("q-drawer",{staticClass:"text-white",attrs:{breakpoint:1152,side:"right","content-class":"bg-primary"},model:{value:t.settings,callback:function(e){t.settings=e},expression:"settings"}},[t.settings?e("q-list",{staticClass:"column justify-start full-height",attrs:{dark:""}},[e("avatar",{staticClass:"col-auto q-py-md",attrs:{size:"72px"}}),e("q-item-section",{staticClass:"col-auto"},[e("calendar-button")],1),e("div",{staticClass:"col row q-pt-lg"},[e("div",{staticClass:"col-auto full-width q-pl-md q-pr-sm"},[e("q-item-section",{staticClass:"q-pt-sm"},[e("q-item-label",{directives:[{name:"t",rawName:"v-t",value:"selectLanguage",expression:"'selectLanguage'"}],staticClass:"q-px-sm",attrs:{header:""}}),e("language-select",{staticClass:"q-pr-xs",model:{value:t.language,callback:function(e){t.language=e},expression:"language"}})],1),e("q-item-section",{staticClass:"q-pt-sm"},[e("q-item-label",{directives:[{name:"t",rawName:"v-t",value:"selectProvider",expression:"'selectProvider'"}],staticClass:"q-px-sm",attrs:{header:""}}),e("provider-select",{staticClass:"q-pr-xs",attrs:{value:t.provider,dark:"",icon:""},on:{input:t.setProvider}})],1),e("q-item-section",{staticClass:"q-pt-lg"},[e("q-item-label",{directives:[{name:"t",rawName:"v-t",value:"animeStatus",expression:"'animeStatus'"}],staticClass:"q-px-sm",attrs:{header:""}}),e("status-select",{staticClass:"q-pr-xs",attrs:{icon:"ondemand_video",options:t.config.airingStatuses},model:{value:t.airingStatusFilter,callback:function(e){t.airingStatusFilter=e},expression:"airingStatusFilter"}})],1),e("q-item-section",{staticClass:"q-pt-sm"},[e("q-item-label",{directives:[{name:"t",rawName:"v-t",value:"animeType",expression:"'animeType'"}],staticClass:"q-px-sm",attrs:{header:""}}),e("status-select",{staticClass:"q-pr-xs",attrs:{icon:"tv",options:t.config.animeTypes},model:{value:t.typeFilter,callback:function(e){t.typeFilter=e},expression:"typeFilter"}})],1),e("q-item-section",{staticClass:"q-pt-sm"},[e("q-item-label",{directives:[{name:"t",rawName:"v-t",value:"genre",expression:"'genre'"}],staticClass:"q-px-sm",attrs:{header:""}}),e("status-select",{staticClass:"q-pr-xs",attrs:{icon:"movie_filter","options-dense":"",and:"",options:t.config.genres},model:{value:t.genreFilter,callback:function(e){t.genreFilter=e},expression:"genreFilter"}})],1)],1),e("div",{staticClass:"col-auto row justify-center q-mt-auto full-width"},[e("reset-button",{staticClass:"col-auto full-width q-pb-sm"})],1)])],1):t._e()],1),e("q-page-container",[e("q-page",{staticClass:"gradient",attrs:{padding:"","style-fn":t.overlappingFooter}},[e("router-view")],1)],1),e("q-footer",{staticClass:"row justify-end fixed-bottom-right",on:{mousedown:function(t){t.preventDefault()}}},[e("q-btn",{staticClass:"square",attrs:{unelevated:"",color:"accent",icon:"description"},on:{click:function(e){t.info=!0}}},[e("q-tooltip",{attrs:{"transition-show":"fade","transition-hide":"fade","content-class":"bg-primary shadow-2"}},[t._v("\n "+t._s(t.$t("aboutApp"))+"\n ")])],1),e("q-dialog",{attrs:{"transition-show":"jump-up","transition-hide":"jump-down"},model:{value:t.info,callback:function(e){t.info=e},expression:"info"}},[e("about",{staticStyle:{width:"1000px","max-width":"80vw"}})],1),e("q-btn",{staticClass:"square",attrs:{square:"",unelevated:"",color:"accent",type:"a",icon:"fab fa-github",href:"https://github.com/Carleslc/MyAnime",target:"_blank"}}),e("support-me",{staticClass:"square q-px-sm"})],1)],1)},i=[],n=s("ded3"),o=s.n(n),r=(s("99af"),s("a861")),c={40:"down"};function l(t){return{mounted:function(){document.addEventListener("keydown",this.keyListener)},beforeDestroy:function(){document.removeEventListener("keydown",this.keyListener)},methods:{keyListener:function(e){var s=e.which||e.keyCode,a=c[s];if(a){var i=t[a];if(i){var n=this[i];n?n.call(this):console.error("Key listener ".concat(i," not found in component methods"))}}}}}}var u=s("2f62"),d={meta:{meta:{"og:image":{property:"og:image",content:"".concat(window.location.href,"/chibi.png")}}},mixins:[r["a"],l({down:"showProviderPopup"})],data:function(){return{settings:!1,info:!1}},computed:o()(o()(o()({},Object(u["e"])("store",["api","userFetched"])),Object(u["c"])("store",["provider","isLoading","isFetched","hasUsername"])),{},{hiddenIfSettings:function(){return this.settings?"display: none":void 0}}),watch:{status:function(){this.updateFetched()},isFetched:function(){this.updateAnimes()},isLoading:function(){this.updateAnimes()}},created:function(){this.info=!this.isRecurringUser},methods:o()(o()(o()({},Object(u["d"])("store",["setProvider","updateFetched"])),Object(u["b"])("store",["fetchAnimes"])),{},{overlappingFooter:function(t){var e="41px";return{minHeight:"calc(100vh - ".concat(t,"px + ").concat(e,")")}},updateAnimes:function(){!this.userFetched||this.isFetched||this.isLoading||!this.hasUsername||this.api.hasError||this.fetchAnimes()},showProviderPopup:function(){this.$refs.providerSelect.showPopup()}})},p=d,m=s("2877"),g=s("4d5a"),f=s("e359"),v=s("65c6"),h=s("9c40"),q=s("429b"),w=s("7460"),b=s("9404"),y=s("1c1c"),C=s("4074"),x=s("0170"),k=s("09e3"),F=s("9989"),j=s("7ff0"),S=s("05c0"),Q=s("24e8"),L=s("eebe"),P=s.n(L),_=Object(m["a"])(p,a,i,!1,null,null,null);e["default"]=_.exports;P()(_,"components",{QLayout:g["a"],QHeader:f["a"],QToolbar:v["a"],QBtn:h["a"],QTabs:q["a"],QTab:w["a"],QDrawer:b["a"],QList:y["a"],QItemSection:C["a"],QItemLabel:x["a"],QPageContainer:k["a"],QPage:F["a"],QFooter:j["a"],QTooltip:S["a"],QDialog:Q["a"]})}}]); -------------------------------------------------------------------------------- /dist/spa/js/2.ec70d99d.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([[2],{"84ba":function(t,e,a){"use strict";a.r(e);a("c96a"),a("b0c0");var s=function(){var t=this,e=t._self._c;return e("q-card",{staticClass:"about"},[e("q-card-section",{staticClass:"row justify-center items-center q-pa-lg"},[e("h1",{staticClass:"text-center"},[t._v("MyAnime")]),e("div",{staticClass:"absolute-right"},[t.small?t._e():e("q-img",{staticClass:"q-ma-md",attrs:{src:"chibi.png",width:"96px"}})],1)]),e("q-separator"),e("q-card-section",{class:"q-py-lg ".concat(t.small?"q-px-lg":"q-px-xl")},[e("i18n",{staticClass:"q-mb-xs",attrs:{path:"about.description",tag:"p"},scopedSlots:t._u([{key:"api",fn:function(){return[e("a",{attrs:{href:t.api.homeUrl,target:"_blank"}},[t._v(t._s(t.api.name))])]},proxy:!0}])}),e("div",{staticClass:"section"},[e("h2",{directives:[{name:"t",rawName:"v-t",value:"about.why.header",expression:"'about.why.header'"}]}),e("div",{domProps:{innerHTML:t._s(t.p("about.why.content",{api:t.api.name}))}})]),e("div",{staticClass:"section"},[e("h2",{directives:[{name:"t",rawName:"v-t",value:"about.how.header",expression:"'about.how.header'"}]}),e("i18n",{attrs:{path:"about.how.enterYourUsername",tag:"p"},scopedSlots:t._u([{key:"api",fn:function(){return[t._v("\n "+t._s(t.api.name)+"\n ")]},proxy:!0},{key:"profileUrl",fn:function(){return[e("i",[t._v(t._s(t.api.profileUrl+"USERNAME"))])]},proxy:!0}])}),e("div",{domProps:{innerHTML:t._s(t.p("about.how.updateEpisode",{api:t.api.name}))}})],1),e("div",{staticClass:"section"},[e("h2",{directives:[{name:"t",rawName:"v-t",value:"about.features.header",expression:"'about.features.header'"}]}),e("ul",{staticClass:"q-gutter-y-sm",domProps:{innerHTML:t._s(t.li("about.features.list",{api:t.api.name}))}}),e("p",{directives:[{name:"t",rawName:"v-t",value:"about.features.note",expression:"'about.features.note'"}]})]),e("div",{staticClass:"section"},[e("h2",{directives:[{name:"t",rawName:"v-t",value:"about.providers.header",expression:"'about.providers.header'"}]}),e("ul",[e("li",[e("a",{attrs:{href:"https://myanimelist.net/",target:"_blank"}},[t._v("MyAnimeList")])]),e("li",[e("a",{attrs:{href:"https://www.crunchyroll.com/",target:"_blank"}},[t._v("Crunchyroll")])]),e("li",[e("a",{attrs:{href:"https://www.netflix.com/",target:"_blank"}},[t._v("Netflix")])]),e("li",[e("a",{attrs:{href:"https://www.google.com/",target:"_blank"}},[t._v("Google")])]),e("li",[t._v("🇬🇧 "),e("a",{attrs:{href:"https://9anime.to/",target:"_blank"}},[t._v("9Anime")])]),e("li",[t._v("🇬🇧 "),e("a",{attrs:{href:"https://zoro.to/",target:"_blank"}},[t._v("Zoro")])]),e("li",[t._v("🇬🇧 "),e("a",{attrs:{href:"https://yugen.to/",target:"_blank"}},[t._v("YugenAnime")])]),e("li",[t._v("🇬🇧 "),e("a",{attrs:{href:"https://gogoanime.bid/",target:"_blank"}},[t._v("GogoAnime")])]),e("li",[t._v("🇬🇧 "),e("a",{attrs:{href:"https://gogoanime.page/",target:"_blank"}},[t._v("GogoAnime.page")])]),e("li",[t._v("🇬🇧 "),e("a",{attrs:{href:"https://animesuge.to/",target:"_blank"}},[t._v("AnimeSuge")])]),e("li",[t._v("🇬🇧 "),e("a",{attrs:{href:"https://animepahe.ru/",target:"_blank"}},[t._v("animepahe")])]),e("li",[t._v("🇬🇧 "),e("a",{attrs:{href:"https://animeheaven.ru/",target:"_blank"}},[t._v("AnimeHeaven")])]),e("li",[t._v("🇬🇧 "),e("a",{attrs:{href:"https://animeflix.live/",target:"_blank"}},[t._v("Animeflix")])]),e("li",[t._v("🇬🇧 "),e("a",{attrs:{href:"https://duckduckgo.com/",target:"_blank"}},[t._v("I'm Feeling Lucky")])]),e("li",[t._v("🇪🇸 "),e("a",{attrs:{href:"https://duckduckgo.com/",target:"_blank"}},[t._v("Voy a tener suerte")])]),e("li",[t._v("🇪🇸 "),e("a",{attrs:{href:"https://www.animeflv.net",target:"_blank"}},[t._v("AnimeFlv")])]),e("li",[t._v("🇪🇸 "),e("a",{attrs:{href:"https://www.animeid.tv/",target:"_blank"}},[t._v("AnimeID")])]),e("li",[t._v("🇪🇸 "),e("a",{attrs:{href:"https://animefenix.tv/",target:"_blank"}},[t._v("AnimeFenix")])]),e("li",[t._v("🇪🇸 "),e("a",{attrs:{href:"http://jkanime.net/",target:"_blank"}},[t._v("jkanime")])]),e("li",[t._v("🇪🇸 "),e("a",{attrs:{href:"https://monoschinos2.com/",target:"_blank"}},[t._v("MonosChinos")])]),e("li",[t._v("🇪🇸 "),e("a",{attrs:{href:"https://animemovil2.com/",target:"_blank"}},[t._v("AnimeMovil")])])]),e("p",{domProps:{innerHTML:t._s(t.p("about.providers.note"))}})]),e("div",{staticClass:"section"},[e("h2",{directives:[{name:"t",rawName:"v-t",value:"about.contact.header",expression:"'about.contact.header'"}]}),e("p",[e("i18n",{attrs:{path:"about.contact.issue",tag:"i"},scopedSlots:t._u([{key:"issue",fn:function(){return[e("a",{attrs:{href:"http://github.com/Carleslc/MyAnime/issues",target:"_blank"}},[t._v(t._s(t.$t("here")))])]},proxy:!0}])})],1),e("p",{directives:[{name:"t",rawName:"v-t",value:"about.contact.note",expression:"'about.contact.note'"}],staticClass:"q-mb-none"})]),e("div",{staticClass:"section"},[e("i",[e("h2",{directives:[{name:"t",rawName:"v-t",value:"about.disclaimer.header",expression:"'about.disclaimer.header'"}]})]),e("div",{domProps:{innerHTML:t._s(t.p("about.disclaimer.content"))}})])],1),e("q-separator"),e("q-card-actions",{staticClass:"scroll q-pa-none",staticStyle:{"max-height":"80vh"},attrs:{align:"center"}},[e("q-btn",{directives:[{name:"close-popup",rawName:"v-close-popup"}],staticClass:"full-width q-pa-sm",attrs:{flat:"",label:"OK",color:"secondary"}})],1)],1)},r=[],i=a("ded3"),n=a.n(i),o=a("2f62"),l=a("002d"),c={computed:n()(n()({},Object(o["e"])("store",["api"])),{},{small:function(){return this.$q.screen.xs}}),methods:{p:function(t,e){return Object(l["b"])(this.$t(t,e),"p")},li:function(t,e){return Object(l["b"])(this.$t(t,e),"li")}}},p=c,h=(a("86c3"),a("2877")),u=a("f09f"),v=a("a370"),m=a("068f"),_=a("eb85"),d=a("4b7e"),b=a("9c40"),f=a("7f67"),g=a("eebe"),w=a.n(g),k=Object(h["a"])(p,s,r,!1,null,null,null);e["default"]=k.exports;w()(k,"components",{QCard:u["a"],QCardSection:v["a"],QImg:m["a"],QSeparator:_["a"],QCardActions:d["a"],QBtn:b["a"]}),w()(k,"directives",{ClosePopup:f["a"]})},"86c3":function(t,e,a){"use strict";a("b801")},b801:function(t,e,a){}}]); -------------------------------------------------------------------------------- /dist/spa/js/20.0aeafe02.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([[20],{"8b24":function(e,t,i){"use strict";i.r(t);var n=function(){var e=this,t=e._self._c;return t("q-infinite-scroll",{ref:"scroll",staticClass:"anime-container",attrs:{offset:1e3},on:{load:e.loadMoreAnimes},scopedSlots:e._u([{key:"loading",fn:function(){return[t("div",{staticClass:"column full-height justify-center items-center"},[t("q-spinner-dots",{attrs:{color:"primary",size:"64px"}})],1)]},proxy:!0}])},e._l(e.animesFilterByStatus,(function(i){return t("anime-episode",{key:i.title,attrs:{anime:i},on:{loaded:e.animeLoaded}})})),1)},s=[],a=i("ded3"),o=i.n(a),r=i("2f62"),l={data:function(){return{animeMounted:0}},computed:o()(o()({},Object(r["e"])("store",["status","api"])),Object(r["c"])("store",["animesFilterByStatus","isLoading","isFetched","hasUsername"])),watch:{animesFilterByStatus:function(){this.animesFilterByStatus.length>this.animeMounted&&this.animeLoading()},status:function(){this.animeMounted=0,this.$refs.scroll.reset()},isFetched:function(){this.isFetched&&this.$refs.scroll.reset()}},methods:o()(o()(o()({},Object(r["d"])("store",["loading","loaded"])),Object(r["b"])("store",["fetchMoreAnimes"])),{},{animeLoading:function(){this.loading(),this.animeMounted>=this.animesFilterByStatus.length&&(this.animeMounted=0)},animeLoaded:function(){this.animeMounted+=1,this.animeMounted===this.animesFilterByStatus.length&&(this.loaded(),this.$refs.scroll.resume())},loadMoreAnimes:function(e,t){var i=this.api.hasError||!this.hasUsername||!this.isFetched;i||this.isLoading?t(i):1===e?t():this.fetchMoreAnimes().then((function(e){t(!e)}))}})},c=l,d=i("2877"),u=i("ef35"),h=i("8380"),m=i("eebe"),f=i.n(m),p=Object(d["a"])(c,n,s,!1,null,null,null);t["default"]=p.exports;f()(p,"components",{QInfiniteScroll:u["a"],QSpinnerDots:h["a"]})}}]); -------------------------------------------------------------------------------- /dist/spa/js/3.f88a54a3.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([[3],{"6e61":function(e,t,n){"use strict";n("f318")},f318:function(e,t,n){},ff1a:function(e,t,n){"use strict";n.r(t);var i=function(){var e=this,t=e._self._c;return t("q-item",{directives:[{name:"ripple",rawName:"v-ripple"}],staticClass:"non-selectable",class:{"icon-only":e.icon&&!e.label},attrs:{clickable:"",dense:e.dense,tag:e.isLink?"a":"span",href:e.isLink?e.href:void 0,target:e.isLink?e.target:void 0,draggable:!e.isLink&&void 0},on:{click:function(t){return e.$emit("click")}}},[e.icon?t("q-item-section",{attrs:{avatar:""}},[t("q-icon",{attrs:{name:e.icon}})],1):e._e(),e.label?t("q-item-section",[t("q-item-label",[e._v(e._s(e.label))]),e.caption?t("q-item-label",{attrs:{caption:""}},[e._v("\n "+e._s(e.caption)+"\n ")]):e._e()],1):e.caption?t("q-tooltip",{attrs:{"transition-show":"fade","transition-hide":"fade"}},[e._v("\n "+e._s(e.caption)+"\n ")]):e._e()],1)},a=[],o={props:{icon:{type:String,default:""},label:{type:String,default:""},href:{type:String,default:"#"},target:{type:String,default:"_self"},caption:{type:String,default:""},dense:{type:Boolean,default:!1}},computed:{isLink:function(){return"#"!==this.href}}},l=o,s=(n("6e61"),n("2877")),c=n("66e5"),r=n("4074"),p=n("0016"),f=n("0170"),u=n("05c0"),d=n("714f"),b=n("eebe"),m=n.n(b),v=Object(s["a"])(l,i,a,!1,null,null,null);t["default"]=v.exports;m()(v,"components",{QItem:c["a"],QItemSection:r["a"],QIcon:p["a"],QItemLabel:f["a"],QTooltip:u["a"]}),m()(v,"directives",{Ripple:d["a"]})}}]); -------------------------------------------------------------------------------- /dist/spa/js/4.e5f9f4f4.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([[4],{"995a":function(t,n,o){"use strict";o("f969")},e02e:function(t,n,o){"use strict";o.r(n);var e=function(){var t=this,n=t._self._c;return n("q-btn",{attrs:{type:"a",href:"https://ko-fi.com/carleslc",target:"_blank",color:"purple",label:"<3",icon:"img:kofi.png"}},[n("q-tooltip",{attrs:{"transition-show":"fade","transition-hide":"fade","content-class":"bg-primary shadow-2 no-scroll"}},[n("div",{domProps:{innerHTML:t._s(t.donate)}})])],1)},a=[],c=o("002d"),i={computed:{donate:function(){return Object(c["c"])(this.$t("donate"))}}},s=i,r=(o("995a"),o("2877")),l=o("9c40"),p=o("05c0"),u=o("0016"),d=o("eebe"),f=o.n(d),b=Object(r["a"])(s,e,a,!1,null,null,null);n["default"]=b.exports;f()(b,"components",{QBtn:l["a"],QTooltip:p["a"],QIcon:u["a"]})},f969:function(t,n,o){}}]); -------------------------------------------------------------------------------- /dist/spa/js/5.f58dc392.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([[5],{"1ce4":function(e,t,o){"use strict";o.r(t);var r=function(){var e=this,t=e._self._c;return t("q-select",{ref:"providerSelect",attrs:{dense:"",standout:!e.standout||e.standout,dark:e.dark,"options-selected-class":"filter-options","options-dark":!1,options:e.providers,"options-dense":e.optionsDense},on:{"popup-show":function(t){return e.focus()},"popup-hide":function(t){return e.focus(!1)}},scopedSlots:e._u([{key:"prepend",fn:function(){return[e.icon?t("q-icon",{attrs:{name:"cast"}}):t("q-avatar",{attrs:{square:"",size:"sm"}},[t("img",{attrs:{src:e.provider.value.icon}})]),e.tooltip?t("q-tooltip",{attrs:{"transition-show":"fade","transition-hide":"fade"}},[t("div",{domProps:{innerHTML:e._s(e.help)}})]):e._e()]},proxy:!0},{key:"option",fn:function(o){return[t("q-item",e._g(e._b({directives:[{name:"ripple",rawName:"v-ripple"}]},"q-item",o.itemProps,!1),o.itemEvents),[t("q-item-section",{attrs:{avatar:""}},[t("q-avatar",{attrs:{square:"",size:"sm"}},[t("img",{attrs:{src:o.opt.value.icon}})])],1),t("q-item-section",[t("q-item-label",[e._v(e._s(o.opt.label))])],1)],1)]}},e.provider&&e.icon?{key:"after",fn:function(){return[t("q-btn",{attrs:{flat:"",dense:"",type:"a",href:e.providerUrl,target:"_blank"},on:{click:e.openProvider}},[t("q-avatar",{attrs:{square:"",size:"sm"}},[t("img",{attrs:{src:e.provider.value.icon}})]),t("q-tooltip",{attrs:{"transition-show":"fade","transition-hide":"fade"}},[e._v("\n "+e._s(e.provider.label)+"\n ")])],1)]},proxy:!0}:null],null,!0),model:{value:e.provider,callback:function(t){e.provider=t},expression:"provider"}})},n=[],i=o("ded3"),s=o.n(i),a=o("b06b"),p=o("2f62"),c=o("002d"),u=o("c01e"),d={mixins:[Object(u["a"])("provider",Object)],props:{icon:{type:Boolean,default:!1},optionsDense:{type:Boolean,default:!1},dark:{type:Boolean,default:!1},tooltip:{type:Boolean,default:!0},standout:{type:String,default:void 0}},computed:s()(s()({},Object(p["c"])("store",["providers"])),{},{providerUrl:function(){return this.provider.value.url},help:function(){return Object(c["c"])(this.$t("providerSelect"))}}),methods:{openProvider:function(){var e=this.providerUrl;e?Object(a["a"])(e):this.showPopup()},showPopup:function(){this.$refs.providerSelect.showPopup()},focus:function(){var e=!(arguments.length>0&&void 0!==arguments[0])||arguments[0];this.$refs.providerSelect.focused=e}}},l=d,v=o("2877"),f=o("ddd8"),m=o("0016"),h=o("cb32"),b=o("05c0"),q=o("66e5"),w=o("4074"),k=o("0170"),y=o("9c40"),_=o("714f"),g=o("eebe"),Q=o.n(g),S=Object(v["a"])(l,r,n,!1,null,null,null);t["default"]=S.exports;Q()(S,"components",{QSelect:f["a"],QIcon:m["a"],QAvatar:h["a"],QTooltip:b["a"],QItem:q["a"],QItemSection:w["a"],QItemLabel:k["a"],QBtn:y["a"]}),Q()(S,"directives",{Ripple:_["a"]})},c01e:function(e,t,o){"use strict";function r(e,t){var o={props:{value:{type:t,required:!0}},computed:{}};return o.computed[e]={get:function(){return this.value},set:function(e){this.$emit("input",e)}},o}o.d(t,"a",(function(){return r}))}}]); -------------------------------------------------------------------------------- /dist/spa/js/6.31eb4eef.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([[6],{"8bcf":function(e,t,n){"use strict";n.r(t);n("a15b"),n("d81d"),n("4de4"),n("d3b7");var o=function(){var e=this,t=e._self._c;return t("q-select",{ref:"statusSelect",attrs:{multiple:"",dense:"",standout:"",dark:"","emit-value":"","options-dense":e.optionsDense,"options-dark":!1,options:e.options,"popup-content-class":"filter-options"},on:{"popup-show":function(t){return e.focus()},"popup-hide":function(t){return e.focus(!1)}},scopedSlots:e._u([{key:"prepend",fn:function(){return[e.icon?t("q-icon",{attrs:{name:e.icon}}):e._e(),e.caption?t("q-tooltip",{attrs:{"transition-show":"fade","transition-hide":"fade"}},[e._v("\n "+e._s(e.caption)+"\n ")]):e._e()]},proxy:!0},e.selected.length===e.allSelected?{key:"selected",fn:function(){return[e._v("\n "+e._s(e.$t(e.allTag))+"\n ")]},proxy:!0}:{key:"selected",fn:function(){return[e._v("\n "+e._s(e.options.filter(e.isSelected).map(e.label).join(", "))+"\n ")]},proxy:!0}],null,!0),model:{value:e.selected,callback:function(t){e.selected=t},expression:"selected"}})},s=[],i=(n("caad"),n("2532"),n("c01e")),c={mixins:[Object(i["a"])("selected",Array)],props:{options:{type:Array,required:!0},optionsDense:{type:Boolean,default:!1},icon:{type:String,default:""},caption:{type:String,default:""},and:{type:Boolean,default:!1}},computed:{allSelected:function(){return this.and?0:this.options.length},allTag:function(){return this.and?"any":"all"}},methods:{isSelected:function(e){return this.selected.includes(this.optValue(e))},optValue:function(e){return e instanceof Object?e.value:e},label:function(e){return e instanceof Object?e.label:e},focus:function(){var e=!(arguments.length>0&&void 0!==arguments[0])||arguments[0];this.$refs.statusSelect.focused=e}}},u=c,l=n("2877"),a=n("ddd8"),r=n("0016"),p=n("05c0"),d=n("eebe"),f=n.n(d),h=Object(l["a"])(u,o,s,!1,null,null,null);t["default"]=h.exports;f()(h,"components",{QSelect:a["a"],QIcon:r["a"],QTooltip:p["a"]})},c01e:function(e,t,n){"use strict";function o(e,t){var n={props:{value:{type:t,required:!0}},computed:{}};return n.computed[e]={get:function(){return this.value},set:function(e){this.$emit("input",e)}},n}n.d(t,"a",(function(){return o}))}}]); -------------------------------------------------------------------------------- /dist/spa/js/7.c01ad7d9.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([[7],{"5f93":function(t,e,n){"use strict";n.r(e);var o=function(){var t=this,e=t._self._c;return e("q-select",{ref:"titleSelect",attrs:{dense:"","options-dense":"",standout:"filter-options","options-selected-class":"filter-options","options-dark":!1,options:t.titles},on:{"popup-show":function(e){return t.focus()},"popup-hide":function(e){return t.focus(!1)}},scopedSlots:t._u([{key:"prepend",fn:function(){return[e("q-icon",{attrs:{name:"list_alt"}})]},proxy:!0}]),model:{value:t.title,callback:function(e){t.title=e},expression:"title"}})},i=[],s=n("c01e"),u={mixins:[Object(s["a"])("title",String)],props:{titles:{type:Array,required:!0}},methods:{focus:function(){var t=!(arguments.length>0&&void 0!==arguments[0])||arguments[0];this.$refs.titleSelect.focused=t}}},r=u,c=n("2877"),l=n("ddd8"),p=n("0016"),a=n("eebe"),f=n.n(a),d=Object(c["a"])(r,o,i,!1,null,null,null);e["default"]=d.exports;f()(d,"components",{QSelect:l["a"],QIcon:p["a"]})},c01e:function(t,e,n){"use strict";function o(t,e){var n={props:{value:{type:e,required:!0}},computed:{}};return n.computed[t]={get:function(){return this.value},set:function(t){this.$emit("input",t)}},n}n.d(e,"a",(function(){return o}))}}]); -------------------------------------------------------------------------------- /dist/spa/kofi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/dist/spa/kofi.png -------------------------------------------------------------------------------- /dist/spa/mal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/dist/spa/mal.png -------------------------------------------------------------------------------- /dist/spa/old/FontSiteSans-Cond-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/dist/spa/old/FontSiteSans-Cond-webfont.woff -------------------------------------------------------------------------------- /dist/spa/old/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/dist/spa/old/background.jpg -------------------------------------------------------------------------------- /dist/spa/old/http.js: -------------------------------------------------------------------------------- 1 | function Options(t){this.apply=function(n){t(n)}}function fetch(t,n,e,o,r){let s={url:n,type:t,cache:!1,success:function(t,n,o){e(o.responseText,o.status,t)},error:function(t,n,e){o&&o(t.responseText,t.status)}};return r&&r.apply(s),$.ajax(s)}function get(t,n,e,o){return fetch("GET",t,n,e,o)}function cors(t,n,e,o,r){return fetch(t,`https://cors-anywhere.herokuapp.com/${n}`,e,o,new Options((t=>{addRequestHeader(t,"Access-Control-Allow-Origin","*"),t.crossDomain=!0})).and(r))}function getCors(t,n,e,o){return cors("GET",t,n,e,o)}var post,postCors;function addRequestHeader(t,n,e){let o=t.beforeSend;t.beforeSend=function(t){o&&o(t),t.setRequestHeader(n,e)}}function contentType(t){return new Options((n=>{addRequestHeader(n,"Content-Type",t)}))}function auth(t,n){return new Options((e=>{addRequestHeader(e,"Authorization","Basic "+btoa(t+":"+n))}))}Options.prototype.and=function(t){let n=this;return t?new Options((e=>{n.apply(e),t.apply(e)})):n},function(){function t(t,n,e,o,r,s){return t("POST",n,o,r,new Options((t=>{t.data=e})).and(s))}post=function(n,e,o,r,s){return t(fetch,n,e,o,r,s)},postCors=function(n,e,o,r,s){return t(cors,n,e,o,r,s)}}(); -------------------------------------------------------------------------------- /dist/spa/old/load-fig.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/dist/spa/old/load-fig.gif -------------------------------------------------------------------------------- /dist/spa/old/mal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/dist/spa/old/mal.jpg -------------------------------------------------------------------------------- /dist/spa/old/storage.js: -------------------------------------------------------------------------------- 1 | var storage;(function(){function t(){this.get=function(t){return localStorage.getItem(t)},this.with=function(t,e){let n=this.get(t);null!==n&&e(n)},this.set=function(t,e){localStorage.setItem(t,e)},this.remove=function(t){localStorage.removeItem(t)}}function e(){this.mem={},this.get=function(t){var e=this.mem[t];return void 0!==e?e:null},this.with=function(t,e){let n=this.get(t);null!==n&&e(n)},this.set=function(t,e){this.mem[t]=e},this.remove=function(t){this.mem[t]=void 0}}storage="undefined"!==typeof Storage?new t:new e})(); -------------------------------------------------------------------------------- /dist/spa/old/utils.js: -------------------------------------------------------------------------------- 1 | function pad(e,t){let n=String(e);while(n.length<(t||2))n="0"+n;return n}function now(){return luxon.DateTime.fromJSDate(new Date)}function formatDate(e){let t=e.weekdayLong,n=e.toLocaleString(luxon.DateTime.DATE_FULL);return`${t} ${n}`}function formatDateTime(e){let t=e.toLocaleString(luxon.DateTime.TIME_24_SIMPLE);return`${formatDate(e)} ~${t}h`}function formatTodayRaw(){let e=new Date;return`${pad(e.getMonth()+1)}${pad(e.getDate())}${e.getFullYear()}`}function asUrl(e,t,n){return t&&(e=`${e}-${t}`),e=e.toLowerCase(),(n||provider)in["gogoanime"]&&(e=e.replace(/;/g,"")),e=e.replace(/[^-a-z0-9]+/g,"-").replace(/-{2,}/,"-"),encodeURIComponent(e)}function idify(e,t){return asUrl(e,null,t||"none")} -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "vueCompilerOptions": { 3 | "target": 2, 4 | "extensions": [".vue"] 5 | }, 6 | "compilerOptions": { 7 | "baseUrl": ".", 8 | "paths": { 9 | "src/*": ["src/*"], 10 | "app/*": ["*"], 11 | "components/*": ["src/components/*"], 12 | "layouts/*": ["src/layouts/*"], 13 | "pages/*": ["src/pages/*"], 14 | "assets/*": ["src/assets/*"], 15 | "boot/*": ["src/boot/*"], 16 | "vue$": ["node_modules/vue/dist/vue.esm.js"] 17 | } 18 | }, 19 | "exclude": ["dist", ".quasar", "node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /old/FontSiteSans-Cond-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/old/FontSiteSans-Cond-webfont.woff -------------------------------------------------------------------------------- /old/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/old/background.jpg -------------------------------------------------------------------------------- /old/http.js: -------------------------------------------------------------------------------- 1 | function Options(fOpts) { 2 | this.apply = function(ajaxOpts) { 3 | fOpts(ajaxOpts); 4 | }; 5 | } 6 | 7 | Options.prototype.and = function(opts) { 8 | let self = this; 9 | return opts ? new Options(ajaxOpts => { 10 | self.apply(ajaxOpts); 11 | opts.apply(ajaxOpts); 12 | }) : self; 13 | } 14 | 15 | function fetch(method, url, success, error, opts) { 16 | //console.log(`${method} ${url}`); 17 | let sendOpts = { 18 | url: url, 19 | type: method, 20 | cache: false, 21 | success: function(response, textStatus, xhr) { 22 | //console.log(`Success (${url})`); 23 | success(xhr.responseText, xhr.status, response); 24 | }, 25 | error: function(xhr, textStatus, errorThrown) { 26 | if (error) { 27 | error(xhr.responseText, xhr.status); 28 | } 29 | } 30 | }; 31 | if (opts) { 32 | opts.apply(sendOpts); 33 | } 34 | return $.ajax(sendOpts); 35 | } 36 | 37 | function get(url, success, error, opts) { 38 | return fetch('GET', url, success, error, opts); 39 | } 40 | 41 | function cors(method, url, success, error, opts) { 42 | return fetch(method, `https://cors-anywhere.herokuapp.com/${url}`, success, error, new Options(ajaxOpts => { 43 | addRequestHeader(ajaxOpts, "Access-Control-Allow-Origin", "*"); 44 | ajaxOpts.crossDomain = true; 45 | }).and(opts)); 46 | } 47 | 48 | function getCors(url, success, error, opts) { 49 | return cors('GET', url, success, error, opts); 50 | } 51 | 52 | var post, postCors; 53 | 54 | (function() { 55 | function _post(base, url, data, success, error, opts) { 56 | return base('POST', url, success, error, new Options(ajaxOpts => { 57 | ajaxOpts.data = data; 58 | }).and(opts)); 59 | } 60 | 61 | post = function(url, data, success, error, opts) { 62 | return _post(fetch, url, data, success, error, opts); 63 | } 64 | 65 | postCors = function(url, data, success, error, opts) { 66 | return _post(cors, url, data, success, error, opts); 67 | } 68 | })(); 69 | 70 | function addRequestHeader(ajaxOpts, name, value) { 71 | let customBeforeSend = ajaxOpts.beforeSend; 72 | ajaxOpts.beforeSend = function(xhr) { 73 | if (customBeforeSend) { 74 | customBeforeSend(xhr); 75 | } 76 | xhr.setRequestHeader(name, value); 77 | }; 78 | } 79 | 80 | function contentType(type) { 81 | return new Options(ajaxOpts => { 82 | addRequestHeader(ajaxOpts, "Content-Type", type); 83 | }); 84 | } 85 | 86 | function auth(user, password) { 87 | return new Options(ajaxOpts => { 88 | addRequestHeader(ajaxOpts, "Authorization", "Basic " + btoa(user + ":" + password)); 89 | }); 90 | } -------------------------------------------------------------------------------- /old/load-fig.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/old/load-fig.gif -------------------------------------------------------------------------------- /old/mal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/old/mal.jpg -------------------------------------------------------------------------------- /old/storage.js: -------------------------------------------------------------------------------- 1 | var storage; 2 | 3 | (function() { 4 | function LocalStorage() { 5 | this.get = function(tag) { 6 | return localStorage.getItem(tag); 7 | }; 8 | this.with = function(tag, callback) { 9 | let value = this.get(tag); 10 | if (value !== null) { 11 | callback(value); 12 | } 13 | }; 14 | this.set = function(tag, value) { 15 | localStorage.setItem(tag, value); 16 | }; 17 | this.remove = function(tag) { 18 | localStorage.removeItem(tag); 19 | }; 20 | } 21 | 22 | function MemStorage() { 23 | this.mem = {}; 24 | this.get = function(tag) { 25 | var value = this.mem[tag]; 26 | return value !== undefined ? value : null; 27 | }; 28 | this.with = function(tag, callback) { 29 | let value = this.get(tag); 30 | if (value !== null) { 31 | callback(value); 32 | } 33 | }; 34 | this.set = function(tag, value) { 35 | this.mem[tag] = value; 36 | }; 37 | this.remove = function(tag) { 38 | this.mem[tag] = undefined; 39 | }; 40 | } 41 | 42 | storage = typeof(Storage) !== "undefined" ? new LocalStorage() : new MemStorage(); 43 | })(); -------------------------------------------------------------------------------- /old/utils.js: -------------------------------------------------------------------------------- 1 | function pad(n, size) { 2 | let s = String(n); 3 | while (s.length < (size || 2)) { 4 | s = "0" + s; 5 | } 6 | return s; 7 | } 8 | 9 | function now() { 10 | return luxon.DateTime.fromJSDate(new Date()); 11 | } 12 | 13 | function formatDate(luxonDate) { 14 | let weekday = luxonDate.weekdayLong; 15 | let date = luxonDate.toLocaleString(luxon.DateTime.DATE_FULL); 16 | return `${weekday} ${date}`; 17 | } 18 | 19 | function formatDateTime(luxonDate) { 20 | let time = luxonDate.toLocaleString(luxon.DateTime.TIME_24_SIMPLE); 21 | return `${formatDate(luxonDate)} ~${time}h`; 22 | } 23 | 24 | function formatTodayRaw() { 25 | let now = new Date(); 26 | return `${pad(now.getMonth() + 1)}${pad(now.getDate())}${now.getFullYear()}`; 27 | } 28 | 29 | function asUrl(s, append, prov) { 30 | if (append) { 31 | s = `${s}-${append}`; 32 | } 33 | s = s.toLowerCase(); 34 | if ((prov || provider) in ["gogoanime"]) { 35 | s = s.replace(/;/g, ''); 36 | } 37 | s = s.replace(/[^-a-z0-9]+/g, '-').replace(/-{2,}/, '-'); 38 | return encodeURIComponent(s); 39 | } 40 | 41 | function idify(s, prov) { 42 | return asUrl(s, null, prov || 'none'); 43 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-anime", 3 | "version": "1.0", 4 | "description": "Watch your favourite animes with your usual provider, synchronized with MyAnimeList.", 5 | "productName": "MyAnime", 6 | "author": "Carleslc ", 7 | "private": true, 8 | "scripts": { 9 | "lint": "eslint --ext .js,.vue --ignore-path .gitignore ./", 10 | "test": "echo \"No test specified\" && exit 0" 11 | }, 12 | "dependencies": { 13 | "@quasar/extras": "^1.12.4", 14 | "axios": "^0.21.2", 15 | "copy-webpack-plugin": "^6.0.3", 16 | "core-js": "^3.0.0", 17 | "eslint-plugin-prettier": "^3.1.3", 18 | "luxon": "^1.24.1", 19 | "prettier": "^2.0.5", 20 | "qs": "^6.9.4", 21 | "quasar": "^1.22.5", 22 | "vue-i18n": "^8.0.0", 23 | "vuex-map-fields": "^1.4.0", 24 | "webpack": "^4.37.0" 25 | }, 26 | "devDependencies": { 27 | "@quasar/app": "^2.4.3", 28 | "babel-eslint": "^10.0.1", 29 | "eslint": "^6.8.0", 30 | "eslint-config-airbnb-base": "^14.0.0", 31 | "eslint-config-prettier": "^6.11.0", 32 | "eslint-loader": "^3.0.3", 33 | "eslint-plugin-import": "^2.20.1", 34 | "eslint-plugin-vue": "^6.1.2" 35 | }, 36 | "engines": { 37 | "node": ">= 10.18.1", 38 | "npm": ">= 6.13.4", 39 | "yarn": ">= 1.21.1" 40 | }, 41 | "browserslist": [ 42 | "last 10 versions, not dead, ie >= 11" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /public/chibi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/public/chibi.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/public/favicon.ico -------------------------------------------------------------------------------- /public/icons/animeflv.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/public/icons/animeflv.ico -------------------------------------------------------------------------------- /public/icons/animepahe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/public/icons/animepahe.png -------------------------------------------------------------------------------- /public/icons/crunchyroll.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/public/icons/crunchyroll.png -------------------------------------------------------------------------------- /public/icons/favicon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/public/icons/favicon-128x128.png -------------------------------------------------------------------------------- /public/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/public/icons/favicon-16x16.png -------------------------------------------------------------------------------- /public/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/public/icons/favicon-32x32.png -------------------------------------------------------------------------------- /public/icons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/public/icons/favicon-96x96.png -------------------------------------------------------------------------------- /public/icons/monoschinos.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/public/icons/monoschinos.ico -------------------------------------------------------------------------------- /public/icons/youtube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/public/icons/youtube.png -------------------------------------------------------------------------------- /public/kofi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/public/kofi.png -------------------------------------------------------------------------------- /public/mal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/public/mal.png -------------------------------------------------------------------------------- /quasar.conf.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file runs in a Node context (it's NOT transpiled by Babel), so use only 3 | * the ES6 features that are supported by your Node version. https://node.green/ 4 | */ 5 | 6 | // Configuration for your app 7 | // https://quasar.dev/quasar-cli/quasar-conf-js 8 | /* eslint-env node */ 9 | /* eslint func-names: 0 */ 10 | /* eslint global-require: 0 */ 11 | 12 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 13 | 14 | module.exports = function (ctx) { 15 | return { 16 | // app boot file (/src/boot) 17 | // --> boot files are part of "main.js" 18 | // https://quasar.dev/quasar-cli/cli-documentation/boot-files 19 | boot: ['i18n', 'components'], 20 | 21 | // https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-css 22 | css: ['app.scss'], 23 | 24 | // https://github.com/quasarframework/quasar/tree/dev/extras 25 | extras: [ 26 | // 'ionicons-v4', 27 | // 'mdi-v5', 28 | // 'eva-icons', 29 | // 'themify', 30 | // 'line-awesome', 31 | 32 | 'roboto-font', 33 | 'material-icons', 34 | 'fontawesome-v5', 35 | ], 36 | 37 | // https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-framework 38 | framework: { 39 | iconSet: 'material-icons', // Quasar icon set 40 | lang: 'en-us', // Quasar language pack 41 | 42 | // Possible values for "all": 43 | // * 'auto' - Auto-import needed Quasar components & directives 44 | // (slightly higher compile time; next to minimum bundle size; most convenient) 45 | // * 'all' - Import everything from Quasar 46 | // (not treeshaking Quasar; biggest bundle size; convenient) 47 | importStrategy: 'auto', 48 | 49 | components: [], 50 | directives: [], 51 | 52 | // Quasar plugins 53 | plugins: ['LocalStorage', 'Notify', 'Meta'], 54 | }, 55 | 56 | // https://quasar.dev/quasar-cli/cli-documentation/supporting-ts 57 | supportTS: false, 58 | 59 | // https://quasar.dev/quasar-cli/cli-documentation/prefetch-feature 60 | // preFetch: true 61 | 62 | // Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-build 63 | build: { 64 | vueRouterMode: 'history', // available values: 'hash', 'history' 65 | publicPath: '', // MyAnime in gh-pages, empty (/) in Netlify 66 | 67 | // rtl: false, // https://quasar.dev/options/rtl-support 68 | // showProgress: false, 69 | // gzip: true, 70 | // analyze: true, 71 | 72 | // Options below are automatically set depending on the env, set them if you want to override 73 | // extractCSS: false, 74 | 75 | // https://quasar.dev/quasar-cli/cli-documentation/handling-webpack 76 | extendWebpack(cfg) { 77 | cfg.module.rules.push({ 78 | enforce: 'pre', 79 | test: /\.(js|vue)$/, 80 | loader: 'eslint-loader', 81 | exclude: /node_modules/, 82 | options: { 83 | formatter: require('eslint').CLIEngine.getFormatter('stylish'), 84 | }, 85 | }); 86 | cfg.resolve.alias = { 87 | ...cfg.resolve.alias, 88 | '@': require('path').resolve(__dirname, 'src'), 89 | }; 90 | if (ctx.prod) { 91 | cfg.plugins.push( 92 | new CopyWebpackPlugin({ 93 | patterns: [{ from: 'old', to: 'old' }], 94 | }) 95 | ); 96 | } 97 | }, 98 | }, 99 | 100 | // Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-devServer 101 | devServer: { 102 | https: false, 103 | port: 8080, 104 | open: true, // opens browser window automatically 105 | }, 106 | 107 | // animations: 'all', // --- includes all animations 108 | // https://quasar.dev/options/animations 109 | animations: [], 110 | 111 | // https://quasar.dev/quasar-cli/developing-ssr/configuring-ssr 112 | ssr: { 113 | pwa: false, 114 | }, 115 | 116 | // https://quasar.dev/quasar-cli/developing-pwa/configuring-pwa 117 | pwa: { 118 | workboxPluginMode: 'GenerateSW', // 'GenerateSW' or 'InjectManifest' 119 | workboxOptions: {}, // only for GenerateSW 120 | manifest: { 121 | name: 'My Anime', 122 | short_name: 'MyAnime', 123 | description: 'Watch your favourite animes with your usual provider, synchronized with MyAnimeList.', 124 | display: 'standalone', 125 | orientation: 'portrait', 126 | background_color: '#ffffff', 127 | theme_color: '#027be3', 128 | icons: [ 129 | { 130 | src: 'icons/icon-128x128.png', 131 | sizes: '128x128', 132 | type: 'image/png', 133 | }, 134 | { 135 | src: 'icons/icon-192x192.png', 136 | sizes: '192x192', 137 | type: 'image/png', 138 | }, 139 | { 140 | src: 'icons/icon-256x256.png', 141 | sizes: '256x256', 142 | type: 'image/png', 143 | }, 144 | { 145 | src: 'icons/icon-384x384.png', 146 | sizes: '384x384', 147 | type: 'image/png', 148 | }, 149 | { 150 | src: 'icons/icon-512x512.png', 151 | sizes: '512x512', 152 | type: 'image/png', 153 | }, 154 | ], 155 | }, 156 | }, 157 | 158 | // Full list of options: https://quasar.dev/quasar-cli/developing-cordova-apps/configuring-cordova 159 | cordova: { 160 | // noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing 161 | id: 'me.carleslc.my-anime', 162 | }, 163 | 164 | // Full list of options: https://quasar.dev/quasar-cli/developing-capacitor-apps/configuring-capacitor 165 | capacitor: { 166 | hideSplashscreen: true, 167 | }, 168 | 169 | // Full list of options: https://quasar.dev/quasar-cli/developing-electron-apps/configuring-electron 170 | electron: { 171 | bundler: 'packager', // 'packager' or 'builder' 172 | 173 | packager: { 174 | // https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options 175 | // OS X / Mac App Store 176 | // appBundleId: '', 177 | // appCategoryType: '', 178 | // osxSign: '', 179 | // protocol: 'myapp://path', 180 | // Windows only 181 | // win32metadata: { ... } 182 | }, 183 | 184 | builder: { 185 | // https://www.electron.build/configuration/configuration 186 | 187 | appId: 'my-anime', 188 | }, 189 | 190 | // More info: https://quasar.dev/quasar-cli/developing-electron-apps/node-integration 191 | nodeIntegration: true, 192 | 193 | extendWebpack(/* cfg */) { 194 | // do something with Electron main process Webpack cfg 195 | // chainWebpack also available besides this extendWebpack 196 | }, 197 | }, 198 | }; 199 | }; 200 | -------------------------------------------------------------------------------- /redirect/README.md: -------------------------------------------------------------------------------- 1 | ## Deploy to GitHub Pages 2 | 3 | ```sh 4 | git subtree push --prefix redirect origin gh-pages 5 | ``` 6 | -------------------------------------------------------------------------------- /redirect/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carleslc/MyAnime/3ced878014522daf4e76dcbdb8462bbff3c57900/redirect/favicon.ico -------------------------------------------------------------------------------- /redirect/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | MyAnime - Redirecting... 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |

Bookmark the new site:

22 | my-anime.netlify.app 23 | 28 |
29 | 32 |
33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /redirect/style.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | height: 100%; 3 | margin: 0; 4 | line-height: 1.5; 5 | background-color: #2d1e53; 6 | } 7 | 8 | .container { 9 | min-height: 100%; 10 | background-image: linear-gradient(#2d1e53, #e242bd); 11 | font-family: Roboto, Helvetica, Arial, sans-serif; 12 | color: white; 13 | } 14 | 15 | .container p, 16 | .container a { 17 | font-size: 1.25em; 18 | } 19 | 20 | .center { 21 | display: flex; 22 | justify-content: center; 23 | align-items: center; 24 | text-align: center; 25 | } 26 | 27 | a#new-site { 28 | color: #f2c037; 29 | font-weight: bold; 30 | text-decoration: none; 31 | } 32 | 33 | a#new-site:hover { 34 | text-decoration: underline; 35 | text-decoration-color: #e242bd; 36 | } 37 | 38 | .logo { 39 | margin-top: 2rem; 40 | } 41 | 42 | .logo img { 43 | width: 10rem; 44 | } 45 | 46 | .footer { 47 | position: fixed; 48 | bottom: 0; 49 | left: 50%; 50 | transform: translate(-50%, -50%); 51 | } 52 | 53 | .text-redirect { 54 | color: #5e3175; 55 | } 56 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | # MyAnime (my-anime) 2 | 3 | Watch your favourite animes synchronized with your usual provider. 4 | 5 | ## Install the dependencies 6 | 7 | ```bash 8 | yarn 9 | ``` 10 | 11 | ## Start the app in development mode (hot-code reloading, error reporting, etc.) 12 | 13 | ### `nvm use` (Node v16.19) 14 | 15 | ```bash 16 | quasar dev 17 | ``` 18 | 19 | ### Node >= v17 20 | 21 | Set before `quasar dev`: 22 | 23 | ```bash 24 | export NODE_OPTIONS=--openssl-legacy-provider 25 | ``` 26 | 27 | ## Lint the files 28 | 29 | ```bash 30 | yarn run lint 31 | ``` 32 | 33 | ## Build the app for production 34 | 35 | ```bash 36 | quasar build 37 | ``` 38 | 39 | ## Serve production app 40 | 41 | ```bash 42 | quasar serve -o --history dist/spa 43 | ``` 44 | 45 | ## Upgrade Quasar 46 | 47 | ```bash 48 | quasar upgrade -i 49 | ``` 50 | 51 | ### Customize the configuration 52 | 53 | See [Configuring quasar.conf.js](https://quasar.dev/quasar-cli/quasar-conf-js). 54 | -------------------------------------------------------------------------------- /src/api/Backend.js: -------------------------------------------------------------------------------- 1 | import { newAxios } from './API'; 2 | 3 | const API_URL = 'https://anime.carleslc.me'; 4 | 5 | export default class Backend { 6 | constructor() { 7 | this.axios = newAxios({ 8 | baseUrl: API_URL, 9 | }); 10 | } 11 | 12 | get(endpoint, headers) { 13 | return this.axios.get(endpoint, headers); 14 | } 15 | 16 | async getCalendar() { 17 | const response = await this.get('/calendar'); 18 | 19 | if (response.status !== 200 || !response.data.airingAnimes) { 20 | return {}; 21 | } 22 | 23 | const airingAnimes = response.data.airingAnimes; 24 | 25 | const calendar = {}; 26 | 27 | airingAnimes.forEach((airingAnime) => { 28 | let anime = calendar[airingAnime.title]; 29 | if (!anime) { 30 | anime = {}; 31 | calendar[airingAnime.title] = anime; 32 | } 33 | anime[airingAnime.episode] = airingAnime.date; 34 | }); 35 | 36 | /* 37 | { 38 | "One Piece": { 39 | "835": "2018-05-06T00:30:00Z", 40 | ... 41 | }, 42 | ... 43 | } 44 | */ 45 | return calendar; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/api/MALSync.js: -------------------------------------------------------------------------------- 1 | import { newAxios } from './API'; 2 | 3 | const API_URL = 'https://api.malsync.moe/'; 4 | 5 | class MALSync { 6 | constructor() { 7 | this.axios = newAxios({ 8 | baseUrl: API_URL, 9 | cors: true, 10 | }); 11 | } 12 | 13 | async getAnimeSite(site, animeId, title, type = 'mal') { 14 | let response; 15 | try { 16 | response = await this.axios.get(`${type}/anime/${animeId}`); 17 | } catch (e) { 18 | console.error('MALSync', e); 19 | return null; 20 | } 21 | const data = response && response.data; 22 | 23 | if (data && data.Sites) { 24 | const siteSync = data.Sites[site]; 25 | 26 | if (siteSync) { 27 | const entries = Object.entries(siteSync); 28 | 29 | let match; 30 | 31 | if (entries.length > 1) { 32 | let i = 0; 33 | 34 | while (!match && i < entries.length) { 35 | const entry = entries[i]; 36 | const value = entry[1]; 37 | 38 | if (value.title === title) { 39 | match = entry; 40 | } 41 | 42 | i += 1; 43 | } 44 | } 45 | 46 | if (!match && entries.length > 0) { 47 | match = entries[0]; 48 | } 49 | 50 | if (match) { 51 | const key = match[0]; 52 | const value = match[1]; 53 | 54 | return { 55 | id: value.identifier || key, 56 | url: value.url, 57 | title: value.title, 58 | }; 59 | } 60 | } 61 | } 62 | 63 | return undefined; 64 | } 65 | } 66 | 67 | export default new MALSync(); 68 | -------------------------------------------------------------------------------- /src/api/MyAnimeList.js: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon'; 2 | import { i18n } from '@/boot/i18n'; 3 | import { Anime } from '@/model/Anime'; 4 | import { notifyError } from '@/utils/errors'; 5 | import { API, encodeParams } from './API'; 6 | 7 | function parseAnimes(animes) { 8 | return animes.map((data) => { 9 | const anime = data.node; 10 | 11 | const fields = { 12 | id: anime.id, 13 | title: anime.title, 14 | alternativeTitles: anime.alternative_titles, 15 | cover: anime.main_picture.medium, 16 | status: anime.my_list_status.status.replace(/_/g, '-'), 17 | type: anime.media_type.toLowerCase(), 18 | genres: anime.genres.map((genre) => genre.name.toLowerCase()), 19 | lastWatchedEpisode: anime.my_list_status.num_episodes_watched, 20 | startDate: anime.start_date, 21 | updatedAt: anime.my_list_status.updated_at, 22 | }; 23 | 24 | if (anime.num_episodes > 0) { 25 | fields.totalEpisodes = anime.num_episodes; 26 | } 27 | 28 | if (anime.status) { 29 | fields.airingStatus = anime.status.replace(/_/g, ' '); 30 | } 31 | 32 | if (anime.broadcast) { 33 | fields.broadcast = { 34 | weekday: anime.broadcast.day_of_the_week, 35 | time: anime.broadcast.start_time, 36 | }; 37 | } 38 | 39 | return new Anime(fields); 40 | }); 41 | } 42 | 43 | const client = '6114d00ca681b7701d1e15fe11a4987e'; 44 | 45 | export class MyAnimeList extends API { 46 | constructor() { 47 | super({ 48 | name: 'MyAnimeList', 49 | image: 'mal.png', 50 | homeUrl: 'https://myanimelist.net/', 51 | profileUrl: 'https://myanimelist.net/profile/', 52 | registerUrl: 'https://myanimelist.net/register.php', 53 | setPasswordUrl: 'https://myanimelist.net/editprofile.php?go=myoptions', 54 | baseUrl: 'https://api.myanimelist.net', 55 | version: 'v2', 56 | headers: { 57 | 'X-MAL-Client-ID': client, 58 | }, 59 | cors: true, 60 | }); 61 | } 62 | 63 | animeUrl(anime) { 64 | return `${this.homeUrl}anime/${anime.id}/`; 65 | } 66 | 67 | async auth(username, password) { 68 | const response = await this.postFormEncoded('/auth/token', { 69 | client_id: client, 70 | grant_type: 'password', 71 | username, 72 | password, 73 | }); 74 | if (response.ok && response.data) { 75 | this.updateAuthInfo(response.data); 76 | } 77 | } 78 | 79 | onError(e) { 80 | if (e.response && e.response.data && e.response.data.error) { 81 | // e.response.status e.response.data.error (e.response.data.message) 82 | // 400 invalid_grant (Incorrect username or password.) 83 | // 403 too_many_failed_login_attempts (Too many failed login attempts. Please try to login again after several hours.) 84 | const errorCode = e.response.data.error; 85 | let message = e.response.data.message; 86 | if (errorCode === 'invalid_grant') { 87 | message = i18n.t('invalidGrant'); 88 | } else if (errorCode === 'too_many_failed_login_attempts') { 89 | message = i18n.t('tooManyFailedLoginAttempts'); 90 | } 91 | notifyError(message); 92 | } else { 93 | super.onError(e); 94 | } 95 | } 96 | 97 | async refreshAccessToken() { 98 | if (!this.refreshToken) { 99 | throw this.needsAuth('Missing refresh token'); 100 | } 101 | const response = await this.postFormEncoded( 102 | '/oauth2/token', 103 | { 104 | client_id: client, 105 | grant_type: 'refresh_token', 106 | refresh_token: this.refreshToken, 107 | }, 108 | { 109 | baseURL: 'https://myanimelist.net/v1', 110 | } 111 | ); 112 | if (response.ok && response.data) { 113 | this.updateAuthInfo(response.data); 114 | } else { 115 | throw this.needsAuth(this.error); 116 | } 117 | } 118 | 119 | updateAuthInfo(data) { 120 | this.commit('setAuthInfo', { 121 | accessToken: data.access_token, 122 | refreshToken: data.refresh_token, 123 | expiration: DateTime.utc().plus({ seconds: data.expires_in }), 124 | }); 125 | } 126 | 127 | async getUserPicture() { 128 | await this.authenticated(); 129 | 130 | const response = await this.get('/users/@me'); 131 | 132 | if (response.ok && response.data) { 133 | return response.data.picture; 134 | } 135 | return null; 136 | } 137 | 138 | // eslint-disable-next-line class-methods-use-this 139 | getUserProfileUrl(username) { 140 | const suffix = username ? `profile/${username}` : ''; 141 | return `https://myanimelist.net/${suffix}`; 142 | } 143 | 144 | async getAnimes(username, status = null, next = false) { 145 | if (next && !this.hasNext(username, status)) { 146 | return []; 147 | } 148 | 149 | await this.authenticated(); 150 | 151 | const currentOffset = this.getCurrentOffset(username, status, next); 152 | 153 | const filters = { 154 | sort: 'list_updated_at', 155 | offset: currentOffset.offset, 156 | limit: 50, 157 | fields: [ 158 | 'id', 159 | 'title', 160 | 'alternative_titles{en,synonyms}', 161 | 'main_picture', 162 | 'media_type', 163 | 'genres', 164 | 'status', 165 | 'start_date', 166 | 'end_date', 167 | 'broadcast', 168 | 'num_episodes', 169 | 'my_list_status{num_episodes_watched,status,updated_at}', 170 | ].join(','), 171 | }; 172 | 173 | if (status) { 174 | filters.status = status.replace(/-/g, '_'); 175 | } 176 | 177 | const response = await this.get(`/users/${username}/animelist?${encodeParams(filters)}`); 178 | 179 | if (!response.ok) { 180 | return []; 181 | } 182 | 183 | if (response.data.paging.next) { 184 | currentOffset.offset += filters.limit; 185 | } else { 186 | currentOffset.hasNext = false; 187 | } 188 | 189 | return parseAnimes(response.data.data); 190 | } 191 | 192 | updateEpisode(anime) { 193 | return this.putFormEncoded(`/anime/${anime.id}/my_list_status`, { 194 | num_watched_episodes: anime.nextEpisode, 195 | status: anime.isLastEpisode ? 'completed' : 'watching', 196 | }); 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/boot/components.js: -------------------------------------------------------------------------------- 1 | export default async ({ Vue }) => { 2 | function kebabCase(s) { 3 | return s 4 | .replace(/([a-z])([A-Z])/g, '$1-$2') 5 | .replace(/\s+/g, '-') 6 | .toLowerCase(); 7 | } 8 | 9 | const ComponentContext = require.context('components/', true, /\.vue$/i, 'lazy'); 10 | 11 | ComponentContext.keys().forEach((componentFilePath) => { 12 | const componentName = kebabCase(componentFilePath.split('/').pop().split('.')[0]); 13 | Vue.component(componentName, () => ComponentContext(componentFilePath)); 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /src/boot/i18n.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueI18n from 'vue-i18n'; 3 | import messages from 'src/i18n'; 4 | import { LocalStorage, Quasar } from 'quasar'; 5 | 6 | const languages = { 7 | en: '🇬🇧', 8 | es: '🇪🇸', 9 | }; 10 | 11 | Vue.use(VueI18n); 12 | 13 | function getLocale() { 14 | const language = LocalStorage.getItem('language'); 15 | if (language && Object.keys(languages).includes(language)) { 16 | return language; 17 | } 18 | return Quasar.lang.getLocale().split('-')[0]; 19 | } 20 | 21 | const i18n = new VueI18n({ 22 | locale: getLocale(), 23 | fallbackLocale: 'en', 24 | messages, 25 | }); 26 | 27 | export default ({ app }) => { 28 | app.i18n = i18n; 29 | }; 30 | 31 | export { i18n }; 32 | 33 | export function getLanguageIcon(language) { 34 | return languages[language]; 35 | } 36 | -------------------------------------------------------------------------------- /src/components/About.vue: -------------------------------------------------------------------------------- 1 | 86 | 87 | 108 | 109 | 142 | -------------------------------------------------------------------------------- /src/components/AnimeSettings.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 76 | -------------------------------------------------------------------------------- /src/components/Avatar.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 41 | -------------------------------------------------------------------------------- /src/components/Back.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 20 | -------------------------------------------------------------------------------- /src/components/CalendarButton.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 21 | -------------------------------------------------------------------------------- /src/components/DynamicButton.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 42 | -------------------------------------------------------------------------------- /src/components/ItemButton.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 39 | 40 | 75 | -------------------------------------------------------------------------------- /src/components/LanguageSelect.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 77 | -------------------------------------------------------------------------------- /src/components/PasswordDialog.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 112 | -------------------------------------------------------------------------------- /src/components/ProviderSelect.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 107 | -------------------------------------------------------------------------------- /src/components/ResetButton.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 24 | -------------------------------------------------------------------------------- /src/components/StatusSelect.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 89 | -------------------------------------------------------------------------------- /src/components/SupportMe.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 27 | 28 | 34 | -------------------------------------------------------------------------------- /src/components/TitleSelect.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 38 | -------------------------------------------------------------------------------- /src/components/UserSearch.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 69 | -------------------------------------------------------------------------------- /src/css/app.scss: -------------------------------------------------------------------------------- 1 | $header-height: 136px; 2 | 3 | body { 4 | * { 5 | font-size: 1rem; 6 | } 7 | background-color: $primary; 8 | } 9 | 10 | .q-page, 11 | .q-footer { 12 | background: transparent; 13 | } 14 | 15 | .q-footer { 16 | width: fit-content; 17 | left: auto; 18 | } 19 | 20 | .q-header { 21 | background-color: $primary; 22 | height: $header-height; 23 | z-index: 5; 24 | 25 | .q-toolbar { 26 | height: 88px; 27 | } 28 | } 29 | 30 | .gradient { 31 | background-image: linear-gradient($primary, $accent); 32 | background-color: $primary; 33 | } 34 | 35 | .q-page-container { 36 | padding-bottom: 0 !important; // overlap footer 37 | } 38 | 39 | .q-btn { 40 | color: white; 41 | } 42 | 43 | $breakpoint-xxs-max: 349px; 44 | 45 | @media (max-width: $breakpoint-xxs-max) { 46 | #settings { 47 | min-width: 30px; 48 | } 49 | 50 | .gt-xxs { 51 | display: none; 52 | } 53 | 54 | .q-drawer__opener { 55 | width: 4px; 56 | } 57 | } 58 | 59 | .q-drawer { 60 | max-width: 100vw; 61 | 62 | .q-item__section { 63 | margin: 0; 64 | } 65 | } 66 | 67 | .q-tooltip { 68 | max-width: 256px; 69 | text-align: center; 70 | font-size: 1em; 71 | background-color: $dark; 72 | z-index: 4000; 73 | 74 | > div, 75 | > p { 76 | font-size: 1.05em; 77 | } 78 | } 79 | 80 | .prefix { 81 | font-size: 16px; 82 | padding-bottom: 2px; 83 | } 84 | 85 | .user-search { 86 | min-width: 192px; 87 | max-width: 25vw; 88 | 89 | .q-field__control { 90 | padding-right: 0; 91 | } 92 | 93 | &.q-field--focused { 94 | .q-btn { 95 | color: white; 96 | background-color: $positive; 97 | } 98 | 99 | .q-btn--flat { 100 | color: $dark; 101 | background-color: transparent; 102 | } 103 | } 104 | 105 | @media (max-width: $breakpoint-xxs-max) { 106 | min-width: 152px; 107 | 108 | .q-btn { 109 | width: 40px; 110 | } 111 | } 112 | } 113 | 114 | .q-select { 115 | .q-field__before, 116 | .q-field__prepend { 117 | padding-right: 12px; 118 | } 119 | .q-field__after, 120 | .q-field__append { 121 | padding-left: 12px; 122 | } 123 | .q-field__inner { 124 | min-width: 42px; 125 | } 126 | } 127 | 128 | .q-select.q-field--standout.q-field--focused { 129 | .filter-options { 130 | background: rgba($primary, 0.1); 131 | .q-field__native { 132 | color: $dark; 133 | } 134 | .q-field__marginal { 135 | color: rgba(0, 0, 0, 0.54); 136 | } 137 | } 138 | } 139 | 140 | .filter-options { 141 | color: grey; 142 | 143 | .q-item--active { 144 | color: $accent; 145 | font-weight: bold; 146 | } 147 | } 148 | 149 | .gt-xsm { 150 | @media (max-width: 750px) { 151 | display: none; 152 | } 153 | } 154 | 155 | h1 { 156 | font-size: 3em; 157 | font-weight: bold; 158 | margin: 0; 159 | } 160 | 161 | h2 { 162 | font-size: 1.5em; 163 | font-weight: 600; 164 | margin: 0.25em 0; 165 | } 166 | 167 | p { 168 | text-align: justify; 169 | } 170 | 171 | a.link { 172 | color: $dark; 173 | text-decoration: none; 174 | &-underline { 175 | color: $primary; 176 | text-decoration: underline; 177 | } 178 | &:hover, 179 | &:active { 180 | color: $accent; 181 | } 182 | } 183 | 184 | .clickable { 185 | cursor: pointer; 186 | } 187 | 188 | .anime-container { 189 | display: grid; 190 | grid-gap: 10px; 191 | // 100% = 2 * margin + cols * width + (cols - 1) * gap 192 | // if margin = 0 then 100% = cols * width + (cols - 1) * gap -> width = (100% - (cols - 1) * gap) / cols 193 | $col-width: calc((100% - (8 - 1) * 10px) / 8); 194 | grid-template-columns: repeat(auto-fill, $col-width); 195 | justify-content: space-between; 196 | 197 | @media (min-width: $breakpoint-xl-min * 1.2) { 198 | $col-width: calc((100% - (12 - 1) * 10px) / 12); 199 | grid-template-columns: repeat(auto-fill, $col-width); 200 | } 201 | 202 | @media (max-width: $breakpoint-md-max) { 203 | $col-width: calc((100% - (6 - 1) * 10px) / 6); 204 | grid-template-columns: repeat(auto-fill, $col-width); 205 | } 206 | 207 | @media (max-width: $breakpoint-sm-max) { 208 | $col-width: calc((100% - (4 - 1) * 10px) / 4); 209 | grid-template-columns: repeat(auto-fill, $col-width); 210 | } 211 | 212 | @media (max-width: $breakpoint-xs-max) { 213 | $col-width: calc((100% - (2 - 1) * 10px) / 2); 214 | grid-template-columns: repeat(auto-fill, $col-width); 215 | } 216 | 217 | @media (max-width: 288px) { 218 | $col-width: 100%; 219 | grid-template-columns: repeat(auto-fill, $col-width); 220 | } 221 | 222 | .anime-episode.q-card { 223 | min-width: 128px; 224 | border-radius: 12px; 225 | overflow: hidden; 226 | box-shadow: none; 227 | outline: 0; 228 | 229 | h1, 230 | h2 { 231 | vertical-align: middle; 232 | text-align: center; 233 | color: white; 234 | text-shadow: 0 1px 0 #000; 235 | line-height: 1.2em; 236 | font-family: Roboto, Helvetica, Arial, sans-serif; 237 | z-index: 1; 238 | } 239 | 240 | h1 { 241 | font-size: 1.2em; 242 | font-weight: bold; 243 | } 244 | 245 | h2 { 246 | font-size: 1em; 247 | } 248 | 249 | &.small { 250 | h1 { 251 | font-size: 1em; 252 | } 253 | h2 { 254 | font-size: 0.8em; 255 | } 256 | } 257 | 258 | .q-btn, 259 | .q-chip { 260 | z-index: 2; 261 | } 262 | 263 | .q-img { 264 | image-rendering: optimizespeed; 265 | image-rendering: -moz-crisp-edges; 266 | image-rendering: -o-crisp-edges; 267 | image-rendering: -webkit-optimize-contrast; 268 | -ms-interpolation-mode: nearest-neighbor; 269 | min-height: 200px; 270 | 271 | @media (max-width: $breakpoint-xs-max) { 272 | min-height: 300px; 273 | } 274 | } 275 | } 276 | } 277 | 278 | .overlay::after { 279 | content: ''; 280 | background: rgba(0, 0, 0, 0.6); 281 | position: absolute; 282 | top: 0; 283 | right: 0; 284 | bottom: 0; 285 | left: 0; 286 | } 287 | 288 | .on-hover { 289 | .hoverable { 290 | opacity: 0; 291 | -webkit-transition: opacity 200ms ease-in-out; 292 | -moz-transition: opacity 200ms ease-in-out; 293 | -o-transition: opacity 200ms ease-in-out; 294 | -ms-transition: opacity 200ms ease-in-out; 295 | transition: opacity 200ms ease-in-out; 296 | } 297 | } 298 | 299 | .on-hover { 300 | &:hover, 301 | &:focus, 302 | &:focus-within { 303 | .hoverable { 304 | opacity: 1; 305 | -webkit-transition: opacity 200ms ease-in-out; 306 | -moz-transition: opacity 200ms ease-in-out; 307 | -o-transition: opacity 200ms ease-in-out; 308 | -ms-transition: opacity 200ms ease-in-out; 309 | transition: opacity 200ms ease-in-out; 310 | } 311 | } 312 | } 313 | 314 | .q-skeleton::before { 315 | content: ''; 316 | } 317 | 318 | .square { 319 | border-radius: 0; 320 | } 321 | 322 | .fit-width { 323 | width: 100%; 324 | } 325 | -------------------------------------------------------------------------------- /src/css/quasar.variables.scss: -------------------------------------------------------------------------------- 1 | // Quasar SCSS (& Sass) Variables 2 | // -------------------------------------------------- 3 | // To customize the look and feel of this app, you can override 4 | // the Sass/SCSS variables found in Quasar's source Sass/SCSS files. 5 | 6 | // Check documentation for full list of Quasar variables 7 | 8 | // Your own variables (that are declared here) and Quasar's own 9 | // ones will be available out of the box in your .vue/.scss/.sass files 10 | 11 | // It's highly recommended to change the default colors 12 | // to match your app's branding. 13 | // Tip: Use the "Theme Builder" on Quasar's documentation website. 14 | 15 | $primary: #2d1e53; 16 | $secondary: #5e3175; 17 | $accent: #e242bd; 18 | 19 | $dark: #1d1d1d; 20 | 21 | $positive: #068b25; 22 | $negative: #c10015; 23 | $info: #31ccec; 24 | $warning: #f2c037; 25 | -------------------------------------------------------------------------------- /src/i18n/en/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | all: 'All', 3 | any: 'Any', 4 | here: 'here', 5 | episode: 'Episode', 6 | complete: 'Complete', 7 | nextEpisode: 'Next episode', 8 | settings: 'Settings', 9 | animeCalendar: 'Anime Calendar', 10 | animeCalendarDescription: 'Calendar anime for this week', 11 | selectLanguage: 'Select language', 12 | selectProvider: 'Select provider', 13 | selectTitle: 'Select alternative title', 14 | animeStatus: 'Anime status', 15 | animeStatusFilter: 'Filter anime status', 16 | alreadyAired: 'Already aired', 17 | notYetAired: 'Not yet aired', 18 | status: { 19 | watching: 'Watching', 20 | onHold: 'On Hold', 21 | planToWatch: 'Plan to Watch' 22 | }, 23 | genre: 'Genre', 24 | genres: { 25 | action: 'Action', 26 | adventure: 'Adventure', 27 | cars: 'Cars', 28 | comedy: 'Comedy', 29 | dementia: 'Dementia', 30 | demons: 'Demons', 31 | drama: 'Drama', 32 | ecchi: 'Ecchi', 33 | fantasy: 'Fantasy', 34 | game: 'Game', 35 | harem: 'Harem', 36 | hentai: 'Hentai', 37 | historical: 'Historical', 38 | horror: 'Horror', 39 | josei: 'Josei', 40 | kids: 'Kids', 41 | magic: 'Magic', 42 | martialArts: 'Martial Arts', 43 | mecha: 'Mecha', 44 | military: 'Military', 45 | music: 'Music', 46 | mystery: 'Mystery', 47 | parody: 'Parody', 48 | police: 'Police', 49 | psychological: 'Psychological', 50 | romance: 'Romance', 51 | samurai: 'Samurai', 52 | school: 'School', 53 | scifi: 'Sci-Fi', 54 | seinen: 'Seinen', 55 | shoujo: 'Shoujo', 56 | shoujoAi: 'Shoujo Ai', 57 | shounen: 'Shounen', 58 | shounenAi: 'Shounen Ai', 59 | sliceOfLife: 'Slice of Life', 60 | space: 'Space', 61 | sports: 'Sports', 62 | superPower: 'Super Power', 63 | supernatural: 'Supernatural', 64 | thriller: 'Thriller', 65 | vampire: 'Vampire', 66 | yaoi: 'Yaoi', 67 | yuri: 'Yuri' 68 | }, 69 | animeType: 'Anime type', 70 | animeTypeFilter: 'Filter anime type', 71 | movie: 'Movie', 72 | special: 'Special', 73 | music: 'Music', 74 | resetSettings: 'Reset settings', 75 | resetSettingsDescription: 'Clean user data and filters', 76 | aboutApp: 'About this app', 77 | donate: ` 78 | This app is completely free and has no ads. 79 | If you like this app, you can support me for the price of a coffee. 80 | Thank you! 81 | `, 82 | providerSelect: ` 83 | Select which provider must be opened by default when clicking over an episode. 84 | Some options are based on search engine, trying to get a proper streamer, but it doesn't mean it always work. 85 | If selected provider cannot find an episode try to change the provider or choose an alternative title. 86 | You can override the default provider in the settings of each anime. 87 | `, 88 | login: 'Log In', 89 | loginDescription: 'Please, log in to your {api} account to view your anime list and update episodes directly within this app.', 90 | notRegisteredYet: 'Not registered yet?', 91 | registerHere: 'Register here', 92 | noPassword: 'If your account has no password because it uses social media login like Facebook, Twitter or Google go to your {0} and set a password for your account first.', 93 | accountSettings: 'Account Settings', 94 | updated: 'Updated {title} to episode {episode}', 95 | completed: "Hooray! You've completed {title}!", 96 | statusChanged: '{title} status changed to {status}', 97 | invalidGrant: 'Incorrect username or password', 98 | tooManyFailedLoginAttempts: 'Too many failed login attempts. Please try to login again after several hours.', 99 | required: { 100 | username: 'Username is required', 101 | password: 'Password is required', 102 | }, 103 | username: 'Username', 104 | password: 'Password', 105 | cancel: 'Cancel', 106 | back: 'Go back', 107 | notFound: 'Sorry, nothing here...', 108 | error: 'Oops... an unexpected error has occurred 😣', 109 | about: { 110 | description: 'Watch your favourite animes with your usual provider, synchronized with {api}.', 111 | why: { 112 | header: 'Why to use?', 113 | content: ` 114 | This is a shortcut to access your anime series with your preferred providers, all in sync with the selected animes in your {api} profile. 115 | Avoid surfing the internet or the providers looking for your episodes, just seat in and enjoy. 116 | With this single page you can test several providers and update your {api} episodes easily. 117 | It also skips some advertising from providers home sites, because you'll access to the anime or episode page directly. 118 | ` 119 | }, 120 | how: { 121 | header: 'How to use', 122 | enterYourUsername: 'Enter your {api} username ({profileUrl}), select your preferred provider and choose one of your animes to watch, then just enjoy your episode.', 123 | updateEpisode: ` 124 | Click on '@:nextEpisode' at the top right corner of an episode to mark it as watched in your {api} profile. 125 | When updating your watched episodes in {api} then the next episode will be shown, waiting for you to be watched. 126 | `, 127 | }, 128 | features: { 129 | header: 'Features', 130 | list: ` 131 | Cover images and links for the series in which you are interested. 132 | Easy access to your next episodes of {api} lists @:status.watching, @:status.onHold and @:status.planToWatch, with several providers for streaming to choose. 133 | Filter episodes by status (@:alreadyAired, @:notYetAired), type (TV, @:movie, OVA, @:special, ONA, @:music) and genre. 134 | Some series have title synonyms, choose between original or alternative title in the settings of the anime. 135 | Update your watched episodes in your {api} profile directly within this page. 136 | `, 137 | note: 'Your preferences are saved in the browser, so you only need to change them when you need it. User is automatically retrieved based on the last user used.' 138 | }, 139 | providers: { 140 | header: 'Supported providers', 141 | note: ` 142 | Some options are based on search engine, trying to get a proper streamer, but it doesn't mean it always work. Sometimes it redirects to another anime, a non-related page, or it is in another language. 143 | If selected provider cannot find an episode try to change the provider or choose an alternative title. 144 | ` 145 | }, 146 | contact: { 147 | header: 'Contact', 148 | issue: 'Your favourite provider is not listed here? Please, open an issue {issue} and we will add it!', 149 | note: 'Open an issue too if you have any doubt, advice or you want to report about something broken.', 150 | }, 151 | disclaimer: { 152 | header: 'Disclaimer', 153 | content: ` 154 | This website does not host any video, it is a client-side website, just linking and sharing content from non-affiliated external providers. 155 | Official providers like Crunchyroll or Netflix are recommended. Use other providers at your own risk. 156 | ` 157 | }, 158 | } 159 | }; 160 | -------------------------------------------------------------------------------- /src/i18n/es/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | all: 'Todo', 3 | any: 'Cualquiera', 4 | here: 'aquí', 5 | episode: 'Episodio', 6 | complete: 'Completar', 7 | nextEpisode: 'Siguiente episodio', 8 | settings: 'Ajustes', 9 | animeCalendar: 'Calendario Anime', 10 | animeCalendarDescription: 'Animes de esta semana', 11 | selectLanguage: 'Seleccionar idioma', 12 | selectProvider: 'Seleccionar proveedor', 13 | selectTitle: 'Seleccionar título alternativo', 14 | animeStatus: 'Estado de anime', 15 | animeStatusFilter: 'Filtrar por estado', 16 | alreadyAired: 'Disponible', 17 | notYetAired: 'No disponible', 18 | status: { 19 | watching: 'Siguiendo', 20 | onHold: 'En espera', 21 | planToWatch: 'En un futuro' 22 | }, 23 | genre: 'Género', 24 | genres: { 25 | action: 'Acción', 26 | adventure: 'Aventuras', 27 | cars: 'Coches', 28 | comedy: 'Comedia', 29 | dementia: 'Demencia', 30 | demons: 'Demonios', 31 | drama: 'Drama', 32 | ecchi: 'Ecchi', 33 | fantasy: 'Fantasía', 34 | game: 'Juegos', 35 | harem: 'Harem', 36 | hentai: 'Hentai', 37 | historical: 'Historia', 38 | horror: 'Horror', 39 | josei: 'Josei', 40 | kids: 'Para niños', 41 | magic: 'Magia', 42 | martialArts: 'Artes marciales', 43 | mecha: 'Mecha', 44 | military: 'Militar', 45 | music: 'Música', 46 | mystery: 'Misterio', 47 | parody: 'Parodia', 48 | police: 'Policíaco', 49 | psychological: 'Psicológico', 50 | romance: 'Romance', 51 | samurai: 'Samurai', 52 | school: 'Escolar', 53 | scifi: 'Ciencia ficción', 54 | seinen: 'Seinen', 55 | shoujo: 'Shoujo', 56 | shoujoAi: 'Shoujo Ai', 57 | shounen: 'Shounen', 58 | shounenAi: 'Shounen Ai', 59 | sliceOfLife: 'Recuentos de la vida', 60 | space: 'Espacial', 61 | sports: 'Deportes', 62 | superPower: 'Superpoderes', 63 | supernatural: 'Supernatural', 64 | thriller: 'Thriller', 65 | vampire: 'Vampiros', 66 | yaoi: 'Yaoi', 67 | yuri: 'Yuri' 68 | }, 69 | animeType: 'Tipo de anime', 70 | animeTypeFilter: 'Filtrar tipo de anime', 71 | movie: 'Película', 72 | special: 'Especial', 73 | music: 'Música', 74 | resetSettings: 'Restablecer ajustes', 75 | resetSettingsDescription: 'Borrar datos de usuario y filtros', 76 | aboutApp: 'Sobre esta aplicación', 77 | donate: ` 78 | Esta aplicación es completamente gratuita y sin anuncios. 79 | Si te gusta la aplicación, puedes apoyarme por el precio de un café. 80 | ¡Muchas gracias! 81 | `, 82 | providerSelect: ` 83 | Selecciona qué proveedor debe abrirse por defecto cuando hagas click en un episodio. 84 | Algunas opciones están basada en buscadores, intentando obtener un proveedor correcto, pero no siempre funciona. 85 | Si el proveedor seleccionado no puede encontrar un episodio prueba con otro proveedor o selecciona un título alternativo. 86 | Puedes sobreescribir el proveedor por defecto en los ajustes de cada anime. 87 | `, 88 | login: 'Iniciar sesión', 89 | loginDescription: 'Por favor, inicia sesión en tu cuenta de {api} para ver tu lista de animes y actualizar los episodios directamente desde esta app.', 90 | notRegisteredYet: '¿Todavía no estás registrado?', 91 | registerHere: 'Regístrate aquí', 92 | noPassword: 'Si tu cuenta no tiene contraseña porque utilizas una red social como Facebook, Twitter o Google para iniciar sesión ve a tus {0} y establece una contraseña primero.', 93 | accountSettings: 'ajustes de cuenta', 94 | updated: '{title} actualizado al episodio {episode}', 95 | completed: "¡Genial! ¡Has completado {title}!", 96 | statusChanged: 'El estado de {title} se ha cambiado a {status}', 97 | invalidGrant: 'Nombre de usuario o contraseña incorrectos', 98 | tooManyFailedLoginAttempts: 'Demasiados intentos fallidos. Prueba a iniciar sesión de nuevo en unas horas.', 99 | required: { 100 | username: 'Introduce tu nombre de usuario', 101 | password: 'Introduce tu contraseña', 102 | }, 103 | username: 'Nombre de usuario', 104 | password: 'Contraseña', 105 | cancel: 'Cancelar', 106 | back: 'Volver', 107 | notFound: 'Lo sentimos, no hay nada aquí...', 108 | error: 'Vaya... ha ocurrido un error inesperado 😣', 109 | about: { 110 | description: 'Disfruta de tus animes favoritos con tu proveedor habitual, sincronizado con {api}.', 111 | why: { 112 | header: '¿Por qué usarme?', 113 | content: ` 114 | Se trata de un acceso directo a los animes de {api} que estás siguiendo con tus páginas web favoritas. 115 | Evita buscar los episodios por internet o los proveedores de anime, simplemente siéntate y disfruta de tu siguiente episodio, esperándote a solo un click. 116 | Desde aquí puedes probar diferentes proveedores de anime y actualizar tus episodios en {api} con un solo click. 117 | También te ahorrarás algunos anuncios de las páginas principales de los proveedores donde ves anime porque accederás directamente a la página del anime o del episodio seleccionado. 118 | ` 119 | }, 120 | how: { 121 | header: 'Cómo usarme', 122 | enterYourUsername: 'Introduce tu nombre de usuario de {api} ({profileUrl}), selecciona tu proveedor favorito y elige uno de tus animes para ver, luego simplemente disfruta de tu episodio.', 123 | updateEpisode: ` 124 | Haz click en '@:nextEpisode' en la esquina superior derecha de un episodio para marcarlo como visto en tu perfil de {api}. 125 | Cuando actualices tus episodios vistos en {api} el siguiente episodio se mostrará, esperándote a que lo veas. 126 | `, 127 | }, 128 | features: { 129 | header: 'Características', 130 | list: ` 131 | Imágenes de portada y enlaces a los animes en los que estás interesado. 132 | Fácil acceso a tus episodios pendientes de tus listas de {api} (@:status.watching, @:status.onHold y @:status.planToWatch), con varias páginas web donde poder verlos. 133 | Filtra episodios por estado (@:alreadyAired, @:notYetAired), tipo (TV, @:movie, OVA, @:special, ONA, @:music) y género. 134 | Algunos animes tienen varios títulos, escoge entre el original o un título alternativo en los ajustes del anime. 135 | Actualiza tus episodios vistos en tu perfil de {api} directamente desde esta página. 136 | `, 137 | note: 'Las preferencias se guardan en tu navegador, así que solo tendrás que cambiarlas cuando lo necesites.' 138 | }, 139 | providers: { 140 | header: 'Proveedores soportados', 141 | note: ` 142 | Algunas opciones se basan en motores de búsqueda, intentando obtener un proveedor adecuado, pero eso no signfica que funcione siempre. Algunas veces te puede redirigir a otro anime, a una página que no tiene nada que ver o simplemente a un episodio en otro idioma. 143 | Si el proveedor seleccionado no puede encontrar un episodio prueba a cambiar de proveedor o selecciona un título alternativo. 144 | ` 145 | }, 146 | contact: { 147 | header: 'Contacto', 148 | issue: '¿Tu proveedor favorito no está listado aquí? Por favor, abre una sugerencia {issue} y lo añadiremos.', 149 | note: 'Abre una sugerencia también si tienes alguna duda, consejo o si encuentras algo que no funciona como debería.', 150 | }, 151 | disclaimer: { 152 | header: 'Atención', 153 | content: ` 154 | Esta página web no almacena ningún vídeo, es solo una aplicación que enlaza el contenido de otras páginas web externas no afiliadas. 155 | Se recomienda utilizar los proveedores oficiales como Crunchyroll o Netflix. Usa otros proveedores bajo tu responsabilidad. 156 | ` 157 | }, 158 | } 159 | }; 160 | -------------------------------------------------------------------------------- /src/i18n/index.js: -------------------------------------------------------------------------------- 1 | import en from './en'; 2 | import es from './es'; 3 | 4 | export default { 5 | en, 6 | es, 7 | }; 8 | -------------------------------------------------------------------------------- /src/index.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= productName %> 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /src/layouts/MainLayout.vue: -------------------------------------------------------------------------------- 1 | 149 | 150 | 212 | -------------------------------------------------------------------------------- /src/mixins/bind.js: -------------------------------------------------------------------------------- 1 | export default function bindValue(name, type) { 2 | const mixin = { 3 | props: { 4 | value: { 5 | type, 6 | required: true, 7 | }, 8 | }, 9 | computed: {}, 10 | }; 11 | 12 | mixin.computed[name] = { 13 | get() { 14 | return this.value; 15 | }, 16 | set(value) { 17 | this.$emit('input', value); 18 | }, 19 | }; 20 | 21 | return mixin; 22 | } 23 | -------------------------------------------------------------------------------- /src/mixins/keyboard.js: -------------------------------------------------------------------------------- 1 | const keys = { 2 | 40: 'down', 3 | }; 4 | 5 | /** @param keyListeners - object mapping key string codes to method names */ 6 | export function registerKeyListeners(keyListeners) { 7 | return { 8 | mounted() { 9 | document.addEventListener('keydown', this.keyListener); 10 | }, 11 | beforeDestroy() { 12 | document.removeEventListener('keydown', this.keyListener); 13 | }, 14 | methods: { 15 | keyListener(e) { 16 | const keyCode = e.which || e.keyCode; 17 | const keyMapping = keys[keyCode]; 18 | if (keyMapping) { 19 | const listenerName = keyListeners[keyMapping]; 20 | if (listenerName) { 21 | const listener = this[listenerName]; 22 | if (listener) { 23 | listener.call(this); 24 | } else { 25 | console.error(`Key listener ${listenerName} not found in component methods`); 26 | } 27 | } 28 | } 29 | }, 30 | }, 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/model/Anime.js: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon'; 2 | 3 | export class Anime { 4 | /** 5 | * @param {Object} anime - anime object 6 | * @param {String} anime.id - provider's anime id 7 | * @param {String} anime.title - main title 8 | * @param {Object} anime.alternativeTitles - alternative titles 9 | * @param {String?} anime.alternativeTitles.en - english title 10 | * @param {Array} anime.alternativeTitles.synonyms - alternative titles 11 | * @param {String} anime.cover - image url 12 | * @param {String} anime.status - watching, on-hold, plan-to-watch 13 | * @param {String} anime.type - tv, ova, movie, special, ona, music, unknown 14 | * @param {Array} anime.genres - action, adventure, cars, comedy, dementia, demons, drama, ecchi, fantasy, game, harem, hentai, historical, horror, josei, kids, magic, martial arts, mecha, military, music, mystery, parody, police, psychological, romance, samurai, school, sci-fi, seinen, shoujo, shoujo ai, shounen, shounen ai, slice of life, space, sports, super power, supernatural, thriller, vampire, yaoi, yuri 15 | * @param {Number?} anime.totalEpisodes - anime total episodes 16 | * @param {String?} anime.startDate - anime start date (yyyy-MM-dd) (JST) 17 | * @param {Object?} anime.broadcast - anime episodes broadcasting 18 | * @param {String} anime.broadcast.weekday - monday to sunday (JST) 19 | * @param {String?} anime.broadcast.time - HH:mm (JST) 20 | * @param {String?} anime.airingStatus - not yet aired, currently airing, finished airing 21 | * @param {String?} anime.updatedAt - last time this anime was updated in user's list 22 | * @param {Number} anime.lastWatchedEpisode - user's last watched episode 23 | */ 24 | constructor({ 25 | id, 26 | title, 27 | alternativeTitles, 28 | cover, 29 | status, 30 | type, 31 | genres, 32 | totalEpisodes, 33 | startDate, 34 | broadcast, 35 | airingStatus, 36 | updatedAt, 37 | lastWatchedEpisode = 0, 38 | }) { 39 | this.id = id; 40 | this.title = title; 41 | this.alternativeTitles = alternativeTitles; 42 | this.cover = cover; 43 | this.status = status; 44 | this.type = type; 45 | this.genres = genres; 46 | this.totalEpisodes = totalEpisodes; 47 | this.broadcast = broadcast; 48 | this.airingStatus = airingStatus; 49 | this.updatedAt = DateTime.fromISO(updatedAt); 50 | this.lastWatchedEpisode = lastWatchedEpisode; 51 | 52 | const titleWithoutDashes = this.title.replace('-', ''); 53 | if (titleWithoutDashes !== title && !this.alternativeTitles.synonyms.includes(titleWithoutDashes)) { 54 | this.alternativeTitles.synonyms.push(titleWithoutDashes); 55 | } 56 | 57 | this.setAiringDate(startDate); 58 | } 59 | 60 | get titles() { 61 | const titles = [this.title]; 62 | if (this.alternativeTitles.en && this.alternativeTitles.en !== this.title) { 63 | titles.push(this.alternativeTitles.en); 64 | } 65 | titles.push(...this.alternativeTitles.synonyms); 66 | return titles; 67 | } 68 | 69 | get hasManyEpisodes() { 70 | return this.type === 'tv' || this.totalEpisodes > 1; 71 | } 72 | 73 | get nextEpisode() { 74 | return this.lastWatchedEpisode + 1; 75 | } 76 | 77 | get isLastEpisode() { 78 | return this.totalEpisodes === this.nextEpisode; 79 | } 80 | 81 | get isCompleted() { 82 | return this.nextEpisode > this.totalEpisodes; 83 | } 84 | 85 | get progress() { 86 | return this.totalEpisodes ? this.nextEpisode / this.totalEpisodes : 0; 87 | } 88 | 89 | get isAired() { 90 | return ( 91 | (this.airingStatus && this.airingStatus !== 'not yet aired') || 92 | (this.airingDate && this.airingDate <= DateTime.local()) 93 | ); 94 | } 95 | 96 | setAiringDate(startDate) { 97 | function fromFormat(format) { 98 | return DateTime.fromFormat(startDate, format, { zone: 'Asia/Tokyo' }); 99 | } 100 | 101 | if (startDate) { 102 | const parts = startDate.split('-').length; 103 | 104 | if (parts === 3) { 105 | this.airingDatePrecision = 'day'; 106 | this.airingDate = fromFormat('yyyy-MM-dd').toLocal(); 107 | } else if (parts === 2) { 108 | this.airingDatePrecision = 'month'; 109 | this.airingDate = fromFormat('yyyy-MM').endOf('month').toLocal(); 110 | } else if (parts === 1) { 111 | this.airingDatePrecision = 'year'; 112 | this.airingDate = fromFormat('yyyy').endOf('year').toLocal(); 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/model/providers/AnimeFLV.js: -------------------------------------------------------------------------------- 1 | import Provider from './Provider.js'; 2 | 3 | // Some urls do not have standard title encoding 4 | const exceptionsMapping = { 5 | 'One Piece': 'one-piece-tv', 6 | }; 7 | 8 | class AnimeFLV extends Provider { 9 | constructor() { 10 | super('https://animeflv.net/', 3, ['es']); 11 | } 12 | 13 | // eslint-disable-next-line class-methods-use-this 14 | get icon() { 15 | return 'icons/animeflv.ico'; 16 | } 17 | 18 | episodeUrl({ anime, title, episode }) { 19 | return `${this.url}ver/${exceptionsMapping[anime.title] || Provider.encode(title)}-${episode}`; 20 | } 21 | } 22 | 23 | /* 24 | withSearch(({ title, episode }) => { 25 | return encodeURIComponent(`site:${this.url} inurl:"/ver" intitle:"${title}" intitle:"Episodio ${episode}"); 26 | }); 27 | */ 28 | 29 | export default new AnimeFLV(); 30 | -------------------------------------------------------------------------------- /src/model/providers/AnimeFenix.js: -------------------------------------------------------------------------------- 1 | import Provider from './Provider.js'; 2 | 3 | class AnimeFenix extends Provider { 4 | constructor() { 5 | super('https://animefenix.tv/', 2, ['es']); 6 | } 7 | 8 | episodeUrl({ title, episode }) { 9 | return `${this.url}ver/${Provider.encode(title)}-${episode}`; 10 | } 11 | } 12 | 13 | export default new AnimeFenix(); 14 | -------------------------------------------------------------------------------- /src/model/providers/AnimeHeaven.js: -------------------------------------------------------------------------------- 1 | import Provider from './Provider.js'; 2 | import { withSearch } from './FeelingLucky'; 3 | 4 | class AnimeHeaven extends Provider { 5 | constructor() { 6 | super('https://animeheaven.ru/', 2, ['en']); 7 | 8 | this.search = withSearch(({ title, episode }) => { 9 | // https://animeheaven.ru/watch/one-piece-sub.79238?ep=163672 10 | // https://animeheaven.ru/watch/one-piece-film-gold 11 | return encodeURIComponent(`site:${this.url}watch intitle:"${title} Episode ${episode}"`); 12 | }); 13 | } 14 | 15 | // eslint-disable-next-line class-methods-use-this 16 | get icon() { 17 | // Alternative: https://static.anmedm.com/static/favicon.ico 18 | return 'https://static.anmedm.com/static/css/animeheaven-logo.png'; 19 | } 20 | 21 | episodeUrl({ anime, title }) { 22 | if (anime.type === 'movie') { 23 | return `${this.url}watch/${encodeURIComponent(Provider.trimSpecials(title))}`; 24 | } 25 | // return this.search.episodeUrl({ anime, title, episode }); 26 | return `${this.url}search?q=${encodeURIComponent(Provider.trimSpecials(title))}`; 27 | } 28 | } 29 | 30 | export default new AnimeHeaven(); 31 | -------------------------------------------------------------------------------- /src/model/providers/AnimeID.js: -------------------------------------------------------------------------------- 1 | import Provider from './Provider.js'; 2 | 3 | class AnimeID extends Provider { 4 | constructor() { 5 | super('https://www.animeid.tv/', 3, ['es']); 6 | } 7 | 8 | episodeUrl({ title, episode }) { 9 | return `${this.url}v/${Provider.encode(title)}-${episode}`; 10 | } 11 | } 12 | 13 | export default new AnimeID(); 14 | -------------------------------------------------------------------------------- /src/model/providers/AnimeMovil2.js: -------------------------------------------------------------------------------- 1 | import Provider from './Provider.js'; 2 | 3 | class AnimeMovil2 extends Provider { 4 | constructor() { 5 | super('https://animemovil2.com/', 3, ['es']); 6 | } 7 | 8 | // eslint-disable-next-line class-methods-use-this 9 | get icon() { 10 | return `${this.url}media/icons/ico.ico`; 11 | } 12 | 13 | episodeUrl({ title, episode }) { 14 | return `${this.url}ver/${Provider.encode(title)}-${episode}`; 15 | } 16 | } 17 | 18 | export default new AnimeMovil2(); 19 | -------------------------------------------------------------------------------- /src/model/providers/AnimeSuge.js: -------------------------------------------------------------------------------- 1 | import MALSync from '@/api/MALSync'; 2 | import { openURL } from 'quasar'; 3 | import { withSearchResolve } from './FeelingLucky'; 4 | import Provider from './Provider.js'; 5 | 6 | class AnimeSuge extends Provider { 7 | constructor(search) { 8 | super('https://animesuge.to/', 2, ['en']); 9 | 10 | this.delegate(search, ['open']); 11 | } 12 | 13 | // eslint-disable-next-line class-methods-use-this 14 | get icon() { 15 | return 'https://animesuge.to/assets/sites/animesuge/icons/favicon.png'; 16 | } 17 | } 18 | 19 | function altEpisodeUrl(animeUrl, { anime, episode }) { 20 | const ep = anime.type === 'movie' ? 'full' : episode; 21 | return `${animeUrl}/ep-${ep}`; 22 | } 23 | 24 | // https://animesuge.to/anime/one-piece-ov8/ep-1049 25 | // https://animesuge.to/anime/one-piece-film-gold-71vy/ep-full 26 | const AnimeSugeLucky = withSearchResolve(({ provider, anime, title }) => { 27 | return encodeURIComponent(`site:${provider.url}anime/${anime.alternativeTitles.en || title}`); 28 | }, altEpisodeUrl); 29 | 30 | export default new AnimeSuge({ 31 | async open(args) { 32 | const { provider, anime, title } = args; 33 | const animeSite = await MALSync.getAnimeSite('9anime', anime.id, title); 34 | if (animeSite) { 35 | openURL(altEpisodeUrl(`${provider.url}anime/${Provider.encode(animeSite.title)}-${animeSite.id}`, args)); // animeSite.id is the same in gogoanime.page and 9anime 36 | } else { 37 | await AnimeSugeLucky.open(args); 38 | } 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /src/model/providers/Animeflix.js: -------------------------------------------------------------------------------- 1 | import Provider from './Provider.js'; 2 | 3 | class Animeflix extends Provider { 4 | constructor() { 5 | super('https://animeflix.live/', 2, ['en']); 6 | } 7 | 8 | episodeUrl({ anime, title, episode }) { 9 | // https://animeflix.live/watch/one-piece-episode-1006/21/ 10 | return `${this.url}watch/${Provider.encode(title)}-episode-${episode}/${anime.id}/`; 11 | } 12 | } 13 | 14 | export default new Animeflix(); 15 | -------------------------------------------------------------------------------- /src/model/providers/Animepahe.js: -------------------------------------------------------------------------------- 1 | import { openURL } from 'quasar'; 2 | import MALSync from '@/api/MALSync'; 3 | import Provider from './Provider.js'; 4 | import { withSearchResolve } from './FeelingLucky'; 5 | 6 | // TODO: API https://animepahe.ru/api?m=search&q=one%20piece 7 | 8 | class Animepahe extends Provider { 9 | constructor(search) { 10 | super('https://animepahe.ru/', 2, ['en']); 11 | 12 | this.delegate(search); 13 | } 14 | 15 | // eslint-disable-next-line class-methods-use-this 16 | get icon() { 17 | return 'icons/animepahe.png'; 18 | } 19 | } 20 | 21 | const AnimepaheLucky = withSearchResolve(({ provider, title, episode }) => { 22 | return encodeURIComponent(`site:${provider.url} "${title}" Ep. "${episode}"`); 23 | }); 24 | 25 | export default new Animepahe({ 26 | async open(args) { 27 | const { anime, title } = args; 28 | const animeSite = await MALSync.getAnimeSite('animepahe', anime.id, title); 29 | 30 | if (animeSite && animeSite.url) { 31 | openURL(animeSite.url); 32 | } else { 33 | await AnimepaheLucky.open(args); 34 | } 35 | }, 36 | }); 37 | -------------------------------------------------------------------------------- /src/model/providers/Crunchyroll.js: -------------------------------------------------------------------------------- 1 | import { openURL } from 'quasar'; 2 | import MALSync from '@/api/MALSync'; 3 | import Provider from './Provider.js'; 4 | 5 | class Crunchyroll extends Provider { 6 | constructor(search) { 7 | super('https://www.crunchyroll.com/', 1); 8 | 9 | this.delegate(search); 10 | } 11 | 12 | // eslint-disable-next-line class-methods-use-this 13 | get icon() { 14 | // `${this.url}favicons/favicon-32x32.png`; 15 | return 'icons/crunchyroll.png'; 16 | } 17 | 18 | episodeUrl({ title }) { 19 | // return `${this.url}search?q=${encodeURI(`${title} ${episode}`)}&o=m&r=f`; 20 | return `${this.url}${Provider.encode(title)}`; 21 | } 22 | } 23 | 24 | export default new Crunchyroll({ 25 | async open(args) { 26 | const { provider, anime, title } = args; 27 | const animeSite = await MALSync.getAnimeSite('Crunchyroll', anime.id, title); 28 | 29 | openURL(animeSite && animeSite.url ? animeSite.url : provider.episodeUrl(args)); 30 | 31 | // TODO: Scrap animeSite.url for episode url 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /src/model/providers/FeelingLucky.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-classes-per-file */ 2 | import { openURL } from 'quasar'; 3 | import { newAxios } from '@/api/API.js'; 4 | import { notifyError } from '@/utils/errors.js'; 5 | import Provider from './Provider.js'; 6 | 7 | export class FeelingLucky extends Provider { 8 | constructor(url, prefix, search, languages) { 9 | super(url, 0, languages); 10 | 11 | this.prefix = prefix; 12 | this.search = search; 13 | } 14 | 15 | episodeUrl(args) { 16 | return this.url + this.prefix + this.search(args); 17 | } 18 | } 19 | 20 | export class FeelingLuckyResolve extends FeelingLucky { 21 | constructor(url, prefix, search, languages) { 22 | super(url, prefix, search, languages); 23 | 24 | this.client = newAxios({ 25 | baseUrl: url + prefix, 26 | }); 27 | } 28 | 29 | /* eslint-disable no-unused-vars */ 30 | /* eslint-disable no-empty-function */ 31 | /* eslint-disable class-methods-use-this */ 32 | async resolve(searchResponse, args) {} // Template 33 | 34 | async open(args) { 35 | const searchResponse = await this.client.get(this.search(args)); 36 | const episodeResolveUrl = await this.resolve(searchResponse, args); 37 | 38 | if (episodeResolveUrl) { 39 | openURL(episodeResolveUrl); 40 | } else { 41 | notifyError(); 42 | } 43 | } 44 | } 45 | 46 | class FeelingDuckyResolve extends FeelingLuckyResolve { 47 | constructor(search, toUrl, languages) { 48 | super('https://api.duckduckgo.com/', '?format=json&no_redirect=1&t=MyAnime&q=!ducky+', search, languages); 49 | 50 | this.toUrl = toUrl || ((episodeRedirectUrl) => episodeRedirectUrl); 51 | } 52 | 53 | async resolve(searchResponse, args) { 54 | const episodeRedirectUrl = searchResponse.data && searchResponse.data.Redirect; 55 | const episodeResolveUrl = episodeRedirectUrl && (await this.toUrl(episodeRedirectUrl, args)); 56 | return episodeResolveUrl || `https://duckduckgo.com/?q=!ducky+${this.search(args)}`; 57 | } 58 | } 59 | 60 | function luckySpanish({ anime, title, episode }) { 61 | const suffix = 'online español -english'; 62 | return encodeURIComponent( 63 | `"${title}"${anime.hasManyEpisodes || episode > 1 ? ` "episodio ${episode}" inurl:${episode}` : ''} ${suffix}` 64 | ); 65 | } 66 | 67 | function luckyEnglish({ anime, title, episode }) { 68 | const suffix = 'online english anime -español'; 69 | return encodeURIComponent( 70 | `"${title}"${anime.hasManyEpisodes || episode > 1 ? ` "episode ${episode}" inurl:${episode}` : ''} ${suffix}` 71 | ); 72 | } 73 | 74 | export function withSearch(search, languages) { 75 | return new FeelingLucky('https://duckduckgo.com/', '?q=!ducky+', search, languages); 76 | } 77 | 78 | export function withSearchResolve(search, toUrl, languages) { 79 | return new FeelingDuckyResolve(search, toUrl, languages); 80 | } 81 | 82 | export const FeelingDuckyES = withSearch(luckySpanish, ['es']); 83 | export const FeelingDuckyEN = withSearch(luckyEnglish, ['en']); 84 | 85 | export const FeelingLuckyES = new FeelingLucky('https://www.google.es/', 'search?btnI&q=', luckySpanish, ['es']); 86 | export const FeelingLuckyEN = new FeelingLucky('https://www.google.com/', 'search?btnI&q=', luckyEnglish, ['en']); 87 | -------------------------------------------------------------------------------- /src/model/providers/Gogoanime.js: -------------------------------------------------------------------------------- 1 | import { openURL } from 'quasar'; 2 | import MALSync from '@/api/MALSync'; 3 | import Provider from './Provider.js'; 4 | import { withSearchResolve } from './FeelingLucky'; 5 | 6 | class Gogoanime extends Provider { 7 | constructor(url, search) { 8 | super(url, 2, ['en']); 9 | 10 | this.delegate(search); 11 | } 12 | 13 | // eslint-disable-next-line class-methods-use-this 14 | get icon() { 15 | // Alternative: https://s2.fastcache.ru/assets/gogoanime/favicon.png 16 | return 'https://cdn.gogocdn.net/files/gogo/img/favicon.png'; 17 | } 18 | } 19 | 20 | // https://gogoanime.news/ 21 | export const GogoanimeOfficial = new Gogoanime('https://gogoanime.bid/', { 22 | episodeUrl({ provider, title, episode }) { 23 | return `${provider.url}${Provider.encode(title)}-episode-${episode}`; 24 | }, 25 | }); 26 | 27 | function altEpisodeUrl(animeUrl, { anime, episode }) { 28 | const ep = anime.type === 'movie' ? 'full' : episode; 29 | return `${animeUrl}/ep-${ep}`; 30 | } 31 | 32 | // https://gogoanime.page/watch/one-piece.ov8/ep-931 33 | // https://gogoanime.page/watch/one-piece-film-gold.71vy/ep-full 34 | const GogoanimeAltLucky = withSearchResolve(({ provider, anime, title }) => { 35 | return encodeURIComponent(`site:${provider.url}anime/${anime.alternativeTitles.en || title}`); 36 | }, altEpisodeUrl); 37 | 38 | export const GogoanimeAlt = new Gogoanime('https://gogoanime.page/', { 39 | async open(args) { 40 | const { provider, anime, title } = args; 41 | const animeSite = await MALSync.getAnimeSite('9anime', anime.id, title); 42 | if (animeSite) { 43 | openURL(altEpisodeUrl(`${provider.url}watch/${Provider.encode(animeSite.title)}.${animeSite.id}`, args)); // animeSite.id is the same in gogoanime.page and 9anime 44 | } else { 45 | await GogoanimeAltLucky.open(args); 46 | } 47 | }, 48 | }); 49 | -------------------------------------------------------------------------------- /src/model/providers/MonosChinos.js: -------------------------------------------------------------------------------- 1 | import Provider from './Provider.js'; 2 | 3 | class MonosChinos extends Provider { 4 | constructor() { 5 | super('https://monoschinos2.com/', 2, ['es']); 6 | } 7 | 8 | // eslint-disable-next-line class-methods-use-this 9 | get icon() { 10 | return 'icons/monoschinos.ico'; 11 | } 12 | 13 | episodeUrl({ title, episode }) { 14 | return `${this.url}ver/${Provider.encode(title)}-${episode}`; 15 | } 16 | } 17 | 18 | export default new MonosChinos(); 19 | -------------------------------------------------------------------------------- /src/model/providers/MyAnimeList.js: -------------------------------------------------------------------------------- 1 | import Provider from './Provider.js'; 2 | 3 | class MyAnimeList extends Provider { 4 | constructor() { 5 | super('https://myanimelist.net/'); 6 | } 7 | 8 | episodeUrl({ anime: { id }, episode }) { 9 | return `${this.url}anime/${id}/-/episode/${episode}`; 10 | } 11 | } 12 | 13 | export default new MyAnimeList(); 14 | -------------------------------------------------------------------------------- /src/model/providers/Netflix.js: -------------------------------------------------------------------------------- 1 | import { openURL } from 'quasar'; 2 | import MALSync from '@/api/MALSync'; 3 | import Provider from './Provider.js'; 4 | 5 | class Netflix extends Provider { 6 | constructor(search) { 7 | super('https://www.netflix.com/'); 8 | 9 | this.delegate(search); 10 | } 11 | 12 | episodeUrl({ title }) { 13 | return `${this.url}search?q=${encodeURI(title)}`; 14 | } 15 | } 16 | 17 | export default new Netflix({ 18 | async open(args) { 19 | const { provider, anime, title } = args; 20 | const animeSite = await MALSync.getAnimeSite('Netflix', anime.id, title); 21 | 22 | openURL(animeSite && animeSite.url ? animeSite.url : provider.episodeUrl(args)); 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /src/model/providers/NineAnime.js: -------------------------------------------------------------------------------- 1 | import { openURL } from 'quasar'; 2 | import MALSync from '@/api/MALSync'; 3 | import Provider from './Provider.js'; 4 | import { withSearchResolve } from './FeelingLucky'; 5 | 6 | class NineAnime extends Provider { 7 | constructor(search) { 8 | super('https://9anime.to/', 2, ['en']); 9 | 10 | this.delegate(search); 11 | } 12 | 13 | // eslint-disable-next-line class-methods-use-this 14 | get icon() { 15 | return `${this.url}assets/sites/9anime/icons/favicon.png`; 16 | } 17 | } 18 | 19 | function toEpisodeUrl(animeUrl, { anime, episode }) { 20 | const ep = anime.type === 'movie' ? 'full' : episode; 21 | return `${animeUrl}/ep-${ep}`; 22 | } 23 | 24 | // https://9anime.to/watch/one-piece.ov8/ep-1006 25 | // https://9anime.to/watch/one-piece-film-gold.71vy/ep-full 26 | const NineAnimeLucky = withSearchResolve(({ provider, anime, title }) => { 27 | return encodeURIComponent(`site:${provider.url}watch/${Provider.encode(anime.alternativeTitles.en || title)}`); 28 | }, toEpisodeUrl); 29 | 30 | export const NineAnimeSync = new NineAnime({ 31 | async open(args) { 32 | const { anime, title } = args; 33 | const animeSite = await MALSync.getAnimeSite('9anime', anime.id, title); 34 | if (animeSite) { 35 | openURL(toEpisodeUrl(animeSite.url, args)); 36 | } else { 37 | await NineAnimeLucky.open(args); 38 | } 39 | }, 40 | }); 41 | 42 | // SEARCH NOT WORKING (403): vrf verification token required 43 | /* 44 | export const NineAnimeSearch = new NineAnime({ 45 | episodeUrl({ provider, anime, title }) { 46 | return `${provider.url}search?keyword=${encodeURIComponent(anime.alternativeTitles.en || title)}`; 47 | }, 48 | }); 49 | */ 50 | -------------------------------------------------------------------------------- /src/model/providers/Provider.js: -------------------------------------------------------------------------------- 1 | export default class Provider { 2 | constructor(url, offset = 0, languages) { 3 | this.url = url; 4 | this.offset = offset; 5 | this.languages = languages; 6 | } 7 | 8 | get icon() { 9 | return `${this.url}favicon.ico`; 10 | } 11 | 12 | /* eslint-disable no-unused-vars */ 13 | 14 | episodeUrl({ anime, title, episode }) { 15 | return this.url; 16 | } 17 | 18 | /* 19 | async open({ anime, title, episode }) { 20 | // Optional. If defined, this will override episodeUrl 21 | } 22 | */ 23 | 24 | delegate(auxProvider, methods = ['episodeUrl', 'open']) { 25 | if (!auxProvider) { 26 | return; 27 | } 28 | 29 | const withProvider = (args) => { 30 | args.provider = this; 31 | return args; 32 | }; 33 | 34 | methods.forEach((method) => { 35 | if (typeof auxProvider[method] === 'function') { 36 | this[method] = (args) => auxProvider[method](withProvider(args)); 37 | } 38 | }); 39 | } 40 | 41 | static encode(s, sep = '-') { 42 | let encoded = encodeURIComponent( 43 | s 44 | .toLowerCase() 45 | .replace(/[^- a-z0-9]+/g, '') 46 | .replace(/\s+/g, sep) 47 | .replace(new RegExp(`(${sep}){2,}`, 'g'), sep) 48 | ); 49 | if (encoded[encoded.length - 1] === sep) { 50 | encoded = encoded.substring(0, encoded.length - 1); 51 | } 52 | return encoded; 53 | } 54 | 55 | static trimSpecials(s) { 56 | return s.replace(/[^- a-zA-Z0-9]+$/, ''); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/model/providers/YouTube.js: -------------------------------------------------------------------------------- 1 | import { FeelingLucky } from '@/model/providers/FeelingLucky'; 2 | 3 | class YouTube extends FeelingLucky { 4 | constructor(search, languages) { 5 | super('https://www.youtube.com/', 'results?search_query=', search, languages); 6 | } 7 | 8 | // eslint-disable-next-line class-methods-use-this 9 | get icon() { 10 | return 'icons/youtube.png'; 11 | } 12 | } 13 | 14 | function luckySpanish({ anime, title, episode }) { 15 | return encodeURIComponent( 16 | `${title}${anime.hasManyEpisodes || episode > 1 ? ` episodio intitle:${episode}` : ''} español` 17 | ); 18 | } 19 | 20 | function luckyEnglish({ anime, title, episode }) { 21 | return encodeURIComponent( 22 | `${title}${anime.hasManyEpisodes || episode > 1 ? ` episode intitle:${episode}` : ''} english` 23 | ); 24 | } 25 | 26 | export const YouTubeES = new YouTube(luckySpanish, ['es']); 27 | export const YouTubeEN = new YouTube(luckyEnglish, ['en']); 28 | -------------------------------------------------------------------------------- /src/model/providers/YugenAnime.js: -------------------------------------------------------------------------------- 1 | import { openURL } from 'quasar'; 2 | import MALSync from '@/api/MALSync'; 3 | import Provider from './Provider.js'; 4 | import { withSearchResolve } from './FeelingLucky'; 5 | 6 | class YugenAnime extends Provider { 7 | constructor(search) { 8 | super('https://yugen.to/', 2, ['en']); 9 | 10 | this.delegate(search); 11 | } 12 | 13 | get icon() { 14 | return `${this.url}static/img/favicon-32x32.png`; 15 | } 16 | } 17 | 18 | // https://yugen.to/watch/798/one-piece/1049/ 19 | const YugenAnimeLucky = withSearchResolve(({ provider, title, episode }) => { 20 | return encodeURIComponent(`site:${provider.url} "${title}" Episode "${episode}"`); 21 | }); 22 | 23 | export default new YugenAnime({ 24 | async open(args) { 25 | const { anime, episode, title } = args; 26 | const animeSite = await MALSync.getAnimeSite('YugenAnime', anime.id, title); 27 | 28 | if (animeSite && animeSite.url) { 29 | openURL(`${animeSite.url.replace('anime', 'watch')}${episode}`); 30 | } else { 31 | await YugenAnimeLucky.open(args); 32 | } 33 | }, 34 | }); 35 | -------------------------------------------------------------------------------- /src/model/providers/Zoro.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-classes-per-file */ 2 | import MALSync from '@/api/MALSync'; 3 | import { newAxios } from '@/api/API.js'; 4 | import { openURL } from 'quasar'; 5 | import { withSearchResolve } from './FeelingLucky'; 6 | import Provider from './Provider.js'; 7 | 8 | class Zoro extends Provider { 9 | constructor(search) { 10 | super('https://zoro.to/', 2, ['en']); 11 | 12 | this.delegate(search); 13 | } 14 | } 15 | 16 | const ENCODED_TITLE_HREF_REGEX = /\/?([- a-z0-9]*-([0-9]+))\?ref=search/; 17 | 18 | // https://zoro.to/watch/one-piece-100?ep=86062 19 | 20 | class ZoroAPI extends Zoro { 21 | constructor() { 22 | super(); 23 | 24 | this.axios = newAxios({ 25 | baseUrl: this.url, 26 | cors: true, 27 | }); 28 | 29 | this.api = newAxios({ 30 | baseUrl: `${this.url}ajax/v2/`, 31 | cors: true, 32 | }); 33 | } 34 | 35 | async open({ anime, title, episode }) { 36 | const animeSite = await MALSync.getAnimeSite('Zoro', anime.id, title); 37 | 38 | let animeEntry; 39 | 40 | if (animeSite) { 41 | animeEntry = animeSite; 42 | animeEntry.url = `${Provider.encode(animeEntry.title)}-${animeEntry.id}`; 43 | } else { 44 | const search = await this.axios.get(`search?keyword=${anime.alternativeTitles.en || title}`); 45 | animeEntry = search.data && ZoroAPI.findAnimeEntry(search.data, anime.title); 46 | } 47 | 48 | let episodeResolveUrl = animeEntry && animeEntry.url && (await this.parseEpisodeUrl(animeEntry, episode)); 49 | 50 | if (!episodeResolveUrl) { 51 | episodeResolveUrl = `${this.url}search?keyword=${encodeURIComponent(title)}+${episode}`; 52 | } 53 | 54 | openURL(episodeResolveUrl); 55 | } 56 | 57 | static findAnimeEntry(data, title) { 58 | const parser = new DOMParser().parseFromString(data, 'text/html'); 59 | 60 | const animes = parser 61 | .querySelectorAll('#main-content .tab-content .flw-item > .film-detail > .film-name > a.dynamic-name') 62 | .values(); 63 | 64 | let { done, value } = animes.next(); 65 | 66 | if (done) { 67 | return null; 68 | } 69 | 70 | let matchEntry; 71 | 72 | do { 73 | const entryTitle = value.getAttribute('data-jname'); 74 | 75 | if (entryTitle === title) { 76 | const hrefMatch = value.getAttribute('href').match(ENCODED_TITLE_HREF_REGEX); 77 | 78 | if (hrefMatch.length > 2) { 79 | matchEntry = { 80 | id: hrefMatch[2], 81 | url: hrefMatch[1], 82 | title: entryTitle, 83 | }; 84 | done = true; 85 | } 86 | } else { 87 | ({ done, value } = animes.next()); 88 | } 89 | } while (!done); 90 | 91 | return matchEntry; 92 | } 93 | 94 | async parseEpisodeUrl(animeEntry, episode) { 95 | const animeUrl = `${this.url}watch/${animeEntry.url}`; 96 | 97 | const episodeList = await this.api.get(`episode/list/${animeEntry.id}`); 98 | 99 | if (!episodeList.data || !episodeList.data.status || episode > episodeList.data.totalItems) { 100 | return animeUrl; 101 | } 102 | 103 | const html = episodeList.data && episodeList.data.html; 104 | 105 | const parser = new DOMParser().parseFromString(html, 'text/html'); 106 | 107 | const episodes = parser.querySelectorAll('a.ssl-item.ep-item'); 108 | 109 | const episodeEntry = episodes.item(episode - 1); 110 | const episodeId = episodeEntry !== null && episodeEntry.getAttribute('data-id'); 111 | 112 | if (episodeId === false) { 113 | return animeUrl; 114 | } 115 | 116 | return `${animeUrl}?ep=${episodeId}`; 117 | } 118 | } 119 | 120 | export const ZoroSearch = new Zoro({ 121 | episodeUrl({ provider, title, episode }) { 122 | return `${provider.url}search?keyword=${encodeURIComponent(title)}+${episode}`; 123 | }, 124 | }); 125 | 126 | export const ZoroLucky = new Zoro( 127 | withSearchResolve(({ provider, anime, title }) => { 128 | return encodeURIComponent(`site:${provider.url}watch ${Provider.encode(anime.alternativeTitles.en || title)}`); 129 | }) 130 | ); 131 | 132 | export const ZoroWithAPI = new ZoroAPI(); 133 | -------------------------------------------------------------------------------- /src/model/providers/jkAnime.js: -------------------------------------------------------------------------------- 1 | import Provider from './Provider.js'; 2 | 3 | class jkAnime extends Provider { 4 | constructor() { 5 | super('https://jkanime.net/', 5, ['es']); 6 | } 7 | 8 | // eslint-disable-next-line class-methods-use-this 9 | get icon() { 10 | return 'https://cdn.jkanime.net/assets/images/favicon.ico'; 11 | } 12 | 13 | episodeUrl({ title, episode }) { 14 | return `${this.url}${Provider.encode(title)}/${episode}/`; 15 | } 16 | } 17 | 18 | // eslint-disable-next-line new-cap 19 | export default new jkAnime(); 20 | -------------------------------------------------------------------------------- /src/pages/Error404.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 19 | -------------------------------------------------------------------------------- /src/pages/Index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 77 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueRouter from 'vue-router'; 3 | 4 | import routes from './routes'; 5 | 6 | Vue.use(VueRouter); 7 | 8 | /* 9 | * If not building with SSR mode, you can 10 | * directly export the Router instantiation; 11 | * 12 | * The function below can be async too; either use 13 | * async/await or return a Promise which resolves 14 | * with the Router instance. 15 | */ 16 | 17 | export default function (/* { store, ssrContext } */) { 18 | const Router = new VueRouter({ 19 | scrollBehavior: () => ({ x: 0, y: 0 }), 20 | routes, 21 | 22 | // Leave these as they are and change in quasar.conf.js instead! 23 | // quasar.conf.js -> build -> vueRouterMode 24 | // quasar.conf.js -> build -> publicPath 25 | mode: process.env.VUE_ROUTER_MODE, 26 | base: process.env.VUE_ROUTER_BASE, 27 | }); 28 | 29 | return Router; 30 | } 31 | -------------------------------------------------------------------------------- /src/router/routes.js: -------------------------------------------------------------------------------- 1 | const routes = [ 2 | { 3 | path: '/', 4 | component: () => import('layouts/MainLayout.vue'), 5 | children: [{ name: 'home', path: '', component: () => import('pages/Index.vue') }], 6 | }, 7 | ]; 8 | 9 | // Always leave this as last one 10 | routes.push({ 11 | path: '*', 12 | component: () => import('pages/Error404.vue') 13 | }); 14 | 15 | export default routes; 16 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | 4 | import store from './store'; 5 | 6 | Vue.use(Vuex); 7 | 8 | export default function () { 9 | const Store = new Vuex.Store({ 10 | modules: { 11 | store 12 | }, 13 | 14 | // enable strict mode (adds overhead!) for dev mode only 15 | strict: process.env.DEV, 16 | }); 17 | 18 | return Store; 19 | } 20 | -------------------------------------------------------------------------------- /src/store/store-flag.d.ts: -------------------------------------------------------------------------------- 1 | // THIS FEATURE-FLAG FILE IS AUTOGENERATED, 2 | // REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING 3 | import "quasar/dist/types/feature-flag"; 4 | 5 | declare module "quasar/dist/types/feature-flag" { 6 | interface QuasarFeatureFlags { 7 | store: true; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/store/store/actions.js: -------------------------------------------------------------------------------- 1 | import { AuthenticationNeededException, notifyError } from '@/utils/errors'; 2 | 3 | function withLoading(commit, promise) { 4 | commit('loading'); 5 | return promise 6 | .catch((e) => { 7 | if (e instanceof AuthenticationNeededException) { 8 | commit('setAuthNeeded', true); 9 | } else { 10 | notifyError(e); 11 | } 12 | }) 13 | .finally(() => { 14 | commit('loaded'); 15 | }); 16 | } 17 | 18 | export default { 19 | async login({ commit, state }, { username, password }) { 20 | commit('setUsername', username); 21 | await withLoading(commit, state.api.auth(username, password)); 22 | }, 23 | fetchAnimes({ commit, state: { api, username, status } }, next = false) { 24 | return withLoading( 25 | commit, 26 | api.getAnimes(username, status, next).then((animes) => { 27 | commit(next ? 'addAnimes' : 'setAnimes', { 28 | status, 29 | animes, 30 | }); 31 | commit('updateFetched'); 32 | return animes; 33 | }) 34 | ); 35 | }, 36 | async fetchMoreAnimes({ dispatch, state: { api, username, status } }) { 37 | if (api.hasNext(username, status)) { 38 | await dispatch('fetchAnimes', true); 39 | } 40 | return api.hasNext(username, status); 41 | }, 42 | fetchCalendar({ commit, state: { backend } }) { 43 | return withLoading( 44 | commit, 45 | backend.getCalendar().then((calendar) => { 46 | commit('setCalendar', calendar); 47 | return calendar; 48 | }) 49 | ); 50 | }, 51 | searchUser({ commit, dispatch, state }, username) { 52 | state.api.commit = commit; 53 | return withLoading( 54 | commit, 55 | new Promise((resolve) => { 56 | commit('setUserFetched', false); 57 | commit('resetAnimes'); 58 | if (username !== state.username) { 59 | commit('logout'); 60 | commit('setUsername', username); 61 | commit('setPicture', state.api.image); 62 | } 63 | dispatch('updatePicture'); 64 | dispatch('fetchAnimes').finally(() => { 65 | if (!state.api.hasError) { 66 | commit('setUserFetched', true); 67 | } 68 | resolve(); 69 | }); 70 | dispatch('fetchCalendar'); 71 | }) 72 | ); 73 | }, 74 | updatePicture({ commit, state: { api, username } }) { 75 | return withLoading( 76 | commit, 77 | api.getUserPicture(username).then((picture) => { 78 | commit('setPicture', picture || api.image); 79 | }) 80 | ); 81 | }, 82 | updateEpisode({ commit, state: { api } }, anime) { 83 | return withLoading( 84 | commit, 85 | api.updateEpisode(anime).then((response) => { 86 | if (response.ok) { 87 | commit('nextEpisode', anime); 88 | commit('setAnimeWatching', anime); 89 | } 90 | return response; 91 | }) 92 | ); 93 | }, 94 | }; 95 | -------------------------------------------------------------------------------- /src/store/store/getters.js: -------------------------------------------------------------------------------- 1 | import { getField } from 'vuex-map-fields'; 2 | import { isBlank } from '@/utils/strings'; 3 | import { providers } from '@/mixins/configuration'; 4 | 5 | export default { 6 | getField, 7 | animesFilterByStatus({ animes, status }) { 8 | return animes[status]; 9 | }, 10 | isFetched({ fetched }) { 11 | return fetched; 12 | }, 13 | isLoading({ loading }) { 14 | return loading > 0; 15 | }, 16 | hasUsername({ username }) { 17 | return !isBlank(username); 18 | }, 19 | providerByAnimeTitle({ providersByAnimeTitle }, getters) { 20 | return (title) => providersByAnimeTitle[title] || getters.provider; 21 | }, 22 | providers({ language }) { 23 | return Object.freeze( 24 | providers.filter((provider) => !provider.value.languages || provider.value.languages.includes(language)) 25 | ); 26 | }, 27 | provider({ providersByLanguage, language }) { 28 | return providersByLanguage[language]; 29 | }, 30 | titleByAnimeId({ api, titlesByAnimeId }) { 31 | return (id) => titlesByAnimeId[api.name][id]; 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /src/store/store/index.js: -------------------------------------------------------------------------------- 1 | import state from './state'; 2 | import mutations from './mutations'; 3 | import getters from './getters'; 4 | import actions from './actions'; 5 | 6 | export default { 7 | namespaced: true, 8 | state, 9 | getters, 10 | mutations, 11 | actions, 12 | }; 13 | -------------------------------------------------------------------------------- /src/store/store/mutations.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import { updateField } from 'vuex-map-fields'; 3 | import { newState } from './state'; 4 | 5 | function sorted({ status, animes }) { 6 | if (status === 'plan-to-watch') { 7 | animes.sort((a, b) => { 8 | if (a.airingStatus === 'currently airing' && b.airingStatus !== 'currently airing') { 9 | return -1; 10 | } 11 | if (b.airingStatus === 'currently airing' && a.airingStatus !== 'currently airing') { 12 | return 1; 13 | } 14 | if (a.airingStatus === 'not yet aired' && b.airingStatus === 'not yet aired' && a.airingDate && b.airingDate) { 15 | return a.airingDate.diff(b.airingDate, 'minutes').toObject().minutes; 16 | } 17 | return b.updatedAt.diff(a.updatedAt, 'minutes').toObject().minutes; 18 | }); 19 | } 20 | return animes; 21 | } 22 | 23 | export default { 24 | updateField, 25 | setAnimes(state, payload) { 26 | state.animes[payload.status] = sorted(payload); 27 | }, 28 | addAnimes(state, payload) { 29 | state.animes[payload.status].push(...sorted(payload)); 30 | }, 31 | resetAnimes(state) { 32 | state.api.resetOffsets(); 33 | state.fetched = false; 34 | state.animes = { 35 | watching: [], 36 | 'on-hold': [], 37 | 'plan-to-watch': [], 38 | }; 39 | }, 40 | updateFetched(state) { 41 | state.fetched = state.api.isFetched(state.username, state.status); 42 | }, 43 | setAPI(state, api) { 44 | state.api = api; 45 | }, 46 | setUsername(state, username) { 47 | state.username = username; 48 | }, 49 | setPicture(state, picture) { 50 | state.picture = picture; 51 | }, 52 | setUserFetched(state, userFetched) { 53 | state.userFetched = userFetched; 54 | }, 55 | nextEpisode(_state, anime) { 56 | anime.lastWatchedEpisode = anime.nextEpisode; 57 | }, 58 | setAnimeWatching(state, anime) { 59 | if (anime.status !== 'watching') { 60 | // Remove from old list 61 | const animeList = state.animes[anime.status]; 62 | animeList.splice(animeList.indexOf(anime), 1); 63 | // Update status 64 | anime.status = 'watching'; 65 | // Add to watching list (if not fetched it will be added later) 66 | if (state.api.isFetched(state.username, anime.status)) { 67 | state.animes.watching.unshift(anime); 68 | } 69 | } 70 | }, 71 | setProvider(state, provider) { 72 | state.providersByLanguage[state.language] = provider; 73 | }, 74 | setProviderByTitle(state, { title, provider }) { 75 | if (provider === state.providersByLanguage[state.language]) { 76 | Vue.delete(state.providersByAnimeTitle, title); 77 | } else { 78 | Vue.set(state.providersByAnimeTitle, title, provider); 79 | } 80 | }, 81 | setAlternativeTitle(state, { anime, title }) { 82 | if (title === anime.title) { 83 | Vue.delete(state.titlesByAnimeId[state.api.name], anime.id); 84 | } else { 85 | Vue.set(state.titlesByAnimeId[state.api.name], anime.id, title); 86 | } 87 | }, 88 | setCalendar(state, calendar) { 89 | state.calendar = calendar; 90 | }, 91 | loading(state) { 92 | state.loading += 1; 93 | }, 94 | loaded(state) { 95 | if (state.loading > 0) { 96 | state.loading -= 1; 97 | } 98 | }, 99 | setAuthNeeded(state, needed) { 100 | state.authNeeded = needed; 101 | }, 102 | clear(state) { 103 | Object.assign(state, newState()); 104 | }, 105 | setAuthInfo({ api }, { accessToken, refreshToken, expiration }) { 106 | // avoids mutation outside vuex store for token refresh 107 | api.accessToken = accessToken; 108 | api.refreshToken = refreshToken; 109 | api.expiration = expiration; 110 | api.updateAuthorization(); 111 | api.saveAuthInfo(); 112 | }, 113 | logout({ api }) { 114 | api.logout(); 115 | }, 116 | }; 117 | -------------------------------------------------------------------------------- /src/store/store/state.js: -------------------------------------------------------------------------------- 1 | import Backend from '@/api/Backend'; 2 | import { MyAnimeList } from '@/api/MyAnimeList'; 3 | import { defaults } from '@/mixins/configuration'; 4 | 5 | export function newState() { 6 | const backend = new Backend(); 7 | const defaultAPI = new MyAnimeList(); 8 | return { 9 | backend, 10 | authNeeded: false, 11 | api: defaultAPI, 12 | picture: defaultAPI.image, 13 | language: defaults.language, 14 | username: defaults.username, 15 | status: defaults.status, 16 | providersByLanguage: defaults.providersByLanguage, 17 | providersByAnimeTitle: defaults.providersByAnimeTitle, 18 | titlesByAnimeId: defaults.titlesByAnimeId, 19 | airingStatusFilter: defaults.airingStatusFilter, 20 | typeFilter: defaults.typeFilter, 21 | genreFilter: defaults.genreFilter, 22 | loading: 0, 23 | fetched: false, 24 | userFetched: false, 25 | animes: { 26 | watching: [], 27 | 'on-hold': [], 28 | 'plan-to-watch': [], 29 | }, 30 | calendar: {}, 31 | }; 32 | } 33 | 34 | export default newState; 35 | -------------------------------------------------------------------------------- /src/utils/errors.js: -------------------------------------------------------------------------------- 1 | import { Notify } from 'quasar'; 2 | import { i18n } from '@/boot/i18n'; 3 | 4 | export function notifyError(message) { 5 | if (message instanceof Error) { 6 | console.error(message.message); 7 | message = undefined; 8 | } 9 | Notify.create({ 10 | type: 'negative', 11 | timeout: 5000, 12 | message: message || i18n.t('error'), 13 | }); 14 | } 15 | 16 | export class AuthenticationNeededException extends Error { 17 | constructor(message) { 18 | super(message || 'Unauthenticated'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/strings.js: -------------------------------------------------------------------------------- 1 | export function trim(s) { 2 | if (s === undefined || s === null) { 3 | return ''; 4 | } 5 | return s.replace(/^\s+|\s+$/g, ''); 6 | } 7 | 8 | export function isBlank(s) { 9 | return trim(s).length === 0; 10 | } 11 | 12 | export function nl2br(s) { 13 | return s.trim().replace(/\r?\n/g, '

'); 14 | } 15 | 16 | export function nl2(s, tag) { 17 | return s 18 | .trim() 19 | .split('\n') 20 | .map((line) => `<${tag}>${line}`) 21 | .join('\n'); 22 | } 23 | 24 | export function pad(s, n = 2, padding = '0') { 25 | s = String(s); 26 | while (s.length < n) { 27 | s = padding + s; 28 | } 29 | return s; 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/subscription.js: -------------------------------------------------------------------------------- 1 | export class SubscriptionQueue { 2 | constructor() { 3 | this.subscribers = []; 4 | } 5 | 6 | subscribe(f) { 7 | this.subscribers.push(f); 8 | } 9 | 10 | consume(...params) { 11 | this.subscribers.forEach((f) => f(...params)); 12 | this.subscribers = []; 13 | } 14 | } 15 | --------------------------------------------------------------------------------