diff --git a/README.md b/README.md index fb125840..de562bd8 100644 --- a/README.md +++ b/README.md @@ -60,16 +60,22 @@ If you want to scrobble / sync from Netflix, this is the only Trakt.tv [plugin]( | Streaming Service | Scrobble | Sync | Limitations | | :---------------: | :------: | :--: | :------------------------------ | | Amazon Prime | ✔️ | ✔️ | - | -| Crunchyroll Beta | ❌ | ✔️ | can't identify movies as movies | +| AMC Plus | ✔️ | ❌ | - | +| Crunchyroll Beta | ❌ | ✔️ | Can't identify movies as movies | | DisneyPlus | ✔️ | ❌ | - | | GoPlay BE | ✔️ | ❌ | - | +| HBO Go | ✔️ | ❌ | - | | HBO Max | ✔️ | ✔️ | - | +| Kijk.nl | ✔️ | ❌ | - | | Netflix | ✔️ | ✔️ | - | | NRK | ✔️ | ✔️ | - | +| Player.pl | ✔️ | ❌ | - | +| Polsatboxgo.pl | ✔️ | ❌ | - | | Streamz BE | ✔️ | ❌ | - | | Viaplay | ✔️ | ✔️ | - | | VRTNu BE | ✔️ | ❌ | - | | VTMGo BE | ✔️ | ❌ | - | +| Wakanim.tv | ✔️ | ❌ | - | diff --git a/package.json b/package.json index 9aa9e36d..12f5d994 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "@babel/preset-typescript": "^7.15.0", "@babel/runtime": "^7.15.4", "@octokit/rest": "^18.10.0", - "@trakt-tools/cli": "^0.3.2", + "@trakt-tools/cli": "^0.3.3", "@types/archiver": "^5.1.1", "@types/circular-dependency-plugin": "^5.0.5", "@types/dotenv-webpack": "^7.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7f22e429..ecc480fb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,7 +18,7 @@ specifiers: '@mui/system': ^5.0.0 '@octokit/rest': ^18.10.0 '@rafaelgomesxyz/axios-rate-limit': ^1.3.1 - '@trakt-tools/cli': ^0.3.2 + '@trakt-tools/cli': ^0.3.3 '@types/archiver': ^5.1.1 '@types/circular-dependency-plugin': ^5.0.5 '@types/dotenv-webpack': ^7.0.3 @@ -110,7 +110,7 @@ devDependencies: '@babel/preset-typescript': 7.15.0_@babel+core@7.15.5 '@babel/runtime': 7.15.4 '@octokit/rest': 18.10.0 - '@trakt-tools/cli': 0.3.2 + '@trakt-tools/cli': 0.3.3 '@types/archiver': 5.1.1 '@types/circular-dependency-plugin': 5.0.5_webpack-cli@4.8.0 '@types/dotenv-webpack': 7.0.3_webpack-cli@4.8.0 @@ -2727,16 +2727,16 @@ packages: axios: 0.26.1 dev: false - /@trakt-tools/cli/0.3.2: + /@trakt-tools/cli/0.3.3: resolution: { - integrity: sha512-y3xCoDPkvS4rmuEKNnDM++fxP6a8u0WSW4Eda/xdjj9tCbqFSUSutgMrVZGB3UljDRRT4mds8TOoFsy5BjWjDQ==, + integrity: sha512-EsR3W6mOCojii3KqL7hjAwq8Rv8GgFi7H/awEEKEUKjzFmy82wvUGNcjrdshHQMp450oM/NvNd5AVbdRVMi/vQ==, } hasBin: true dependencies: commander: 7.2.0 inquirer: 8.1.0 - prettier: 2.3.1 + prettier: 2.4.1 dev: true /@tsconfig/node10/1.0.8: @@ -3462,7 +3462,10 @@ packages: dev: true /ansi-regex/2.1.1: - resolution: { integrity: sha1-w7M6te42DYbg5ijwRorn7yfWVN8= } + resolution: + { + integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==, + } engines: { node: '>=0.10.0' } dev: true @@ -4193,7 +4196,10 @@ packages: dev: true /clone/1.0.4: - resolution: { integrity: sha1-2jCcwmPfFZlMaIypAheco8fNfH4= } + resolution: + { + integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==, + } engines: { node: '>=0.8' } dev: true @@ -4224,7 +4230,10 @@ packages: dev: true /color-name/1.1.3: - resolution: { integrity: sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= } + resolution: + { + integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==, + } /color-name/1.1.4: resolution: @@ -4522,7 +4531,10 @@ packages: dev: true /decache/3.1.0: - resolution: { integrity: sha1-T1A2+9ZYH8yXI3rDlUokS5U2wto= } + resolution: + { + integrity: sha512-p7D6wJ5EJFFq1CcF2lu1XeqKFLBob8jRQGNAvFLTsV3CbSKBl3VtliAVlUIGz2i9H6kEFnI2Amaft5ZopIG2Fw==, + } dependencies: find: 0.2.9 dev: false @@ -4541,7 +4553,10 @@ packages: dev: false /defaults/1.0.3: - resolution: { integrity: sha1-xlYFHpgX2f8I7YgUd/P+QBnz730= } + resolution: + { + integrity: sha512-s82itHOnYrN0Ib8r+z7laQz3sdE+4FP3d9Q7VLO7U+KRT+CR0GsWuyHxzdAY82I7cXv0G/twrqomTJLOssO5HA==, + } dependencies: clone: 1.0.4 dev: true @@ -4856,7 +4871,10 @@ packages: dev: true /escape-string-regexp/1.0.5: - resolution: { integrity: sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= } + resolution: + { + integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==, + } engines: { node: '>=0.8.0' } /escape-string-regexp/4.0.0: @@ -5460,7 +5478,10 @@ packages: dev: true /has-flag/3.0.0: - resolution: { integrity: sha1-tdRU3CGZriJWmfNGfloH87lVuv0= } + resolution: + { + integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==, + } engines: { node: '>=4' } /has-flag/4.0.0: @@ -6667,7 +6688,10 @@ packages: dev: true /os-tmpdir/1.0.2: - resolution: { integrity: sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= } + resolution: + { + integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==, + } engines: { node: '>=0.10.0' } dev: true @@ -6968,15 +6992,6 @@ packages: fast-diff: 1.2.0 dev: true - /prettier/2.3.1: - resolution: - { - integrity: sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA==, - } - engines: { node: '>=10.13.0' } - hasBin: true - dev: true - /prettier/2.4.1: resolution: { @@ -8064,7 +8079,10 @@ packages: dev: true /strip-ansi/3.0.1: - resolution: { integrity: sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= } + resolution: + { + integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==, + } engines: { node: '>=0.10.0' } dependencies: ansi-regex: 2.1.1 @@ -8235,7 +8253,10 @@ packages: dev: true /through/2.3.8: - resolution: { integrity: sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= } + resolution: + { + integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==, + } dev: true /tiny-invariant/1.1.0: @@ -8545,7 +8566,10 @@ packages: dev: true /wcwidth/1.0.1: - resolution: { integrity: sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g= } + resolution: + { + integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==, + } dependencies: defaults: 1.0.3 dev: true diff --git a/src/common/ScrobbleParser.ts b/src/common/ScrobbleParser.ts index f413c53b..e7e45755 100644 --- a/src/common/ScrobbleParser.ts +++ b/src/common/ScrobbleParser.ts @@ -285,8 +285,12 @@ export abstract class ScrobbleParser { } protected parseItemIdFromUrl(): string | null { - const { id = null } = this.options.watchingUrlRegex?.exec(this.getLocation())?.groups ?? {}; - return id; + const { + id = null, + episodeId = null, + movieId = null, + } = this.options.watchingUrlRegex?.exec(this.getLocation())?.groups ?? {}; + return episodeId || movieId || id; } protected async parseItemIdFromInjectedScript(): Promise { diff --git a/src/services/amc-plus/AmcPlusApi.ts b/src/services/amc-plus/AmcPlusApi.ts new file mode 100644 index 00000000..15809e2b --- /dev/null +++ b/src/services/amc-plus/AmcPlusApi.ts @@ -0,0 +1,161 @@ +import { AmcPlusService } from '@/amc-plus/AmcPlusService'; +import { ServiceApi, ServiceApiSession } from '@apis/ServiceApi'; +import { Requests, withHeaders } from '@common/Requests'; +import { ScriptInjector } from '@common/ScriptInjector'; +import { Shared } from '@common/Shared'; +import { EpisodeItem, MovieItem, ScrobbleItem } from '@models/Item'; + +export interface AmcPlusSession extends ServiceApiSession { + auth: { + accessToken: string; + }; +} + +export interface AmcPlusItemResponse { + data: { + properties: AmcPlusEpisodeResponse | AmcPlusMovieResponse; + }; +} + +export interface AmcPlusEpisodeResponse { + pageType: 'episode'; + pageTitle: string; + id: string; + + // In slug-ish format (e.g. "the-walking-dead") + showName: string; + + seasonNumber: number; + episodeNumber: number; +} + +export interface AmcPlusMovieResponse { + pageType: 'movie'; + pageTitle: string; + id: string; +} + +class _AmcPlusApi extends ServiceApi { + HOST_URL = 'https://www.amcplus.com/foryou'; + API_URL = 'https://gw.cds.amcn.com/content-compiler-cr/api/v1'; + + authRequests = Requests; + + isActivated = false; + session?: AmcPlusSession | null; + + constructor() { + super(AmcPlusService.id); + } + + async activate() { + if (this.session === null) { + return; + } + + try { + const partialSession = await this.getSession(); + if (!partialSession || !partialSession.auth || !partialSession.auth.accessToken) { + throw new Error(); + } + + this.authRequests = withHeaders({ + Authorization: `Bearer ${partialSession.auth.accessToken}`, + }); + + this.session = { + auth: { + accessToken: partialSession.auth.accessToken, + }, + profileName: null, + }; + + this.isActivated = true; + } catch (err) { + this.session = null; + } + } + + async getItem(id: string): Promise { + let item: ScrobbleItem | null = null; + if (!this.isActivated) { + await this.activate(); + } + if (!this.session) { + throw new Error('Invalid session'); + } + try { + const responseText = await this.authRequests.send({ + url: `${this.API_URL}/content/amcn/amcplus/path/${id}?`, + method: 'GET', + }); + const response = JSON.parse(responseText) as AmcPlusItemResponse; + + item = this.parseMetadata(response.data.properties); + } catch (err) { + if (Shared.errors.validate(err)) { + Shared.errors.error('Failed to get item.', err); + } + } + return item; + } + + parseMetadata(metadata: AmcPlusEpisodeResponse | AmcPlusMovieResponse): ScrobbleItem { + let item: ScrobbleItem; + const serviceId = this.id; + const { pageType: type, pageTitle: title, id } = metadata; + if (type === 'episode') { + const { showName: showSlug, seasonNumber: season, episodeNumber: number } = metadata; + const showTitle = this.formatSlug(showSlug); + + item = new EpisodeItem({ + serviceId, + id, + title, + season, + number, + show: { + serviceId, + title: showTitle, + }, + }); + } else { + item = new MovieItem({ + serviceId, + id, + title, + }); + } + return item; + } + + formatSlug(slug: string): string { + return slug + .split('-') + .map((word) => `${word[0].toUpperCase()}${word.slice(1).toLowerCase()}`) + .join(' '); + } + + async getSession(): Promise | null> { + const result = await ScriptInjector.inject>( + this.id, + 'session', + this.HOST_URL, + () => { + const session: Partial = {}; + + const accessToken = window.localStorage.getItem('access_token'); + if (accessToken) { + session.auth = { + accessToken, + }; + } + + return session; + } + ); + return result; + } +} + +export const AmcPlusApi = new _AmcPlusApi(); diff --git a/src/services/amc-plus/AmcPlusParser.ts b/src/services/amc-plus/AmcPlusParser.ts new file mode 100644 index 00000000..49e1e96c --- /dev/null +++ b/src/services/amc-plus/AmcPlusParser.ts @@ -0,0 +1,18 @@ +import { AmcPlusApi } from '@/amc-plus/AmcPlusApi'; +import { ScrobbleParser } from '@common/ScrobbleParser'; + +class _AmcPlusParser extends ScrobbleParser { + constructor() { + super(AmcPlusApi, { + /** + * Example Formats: + * + * - Episodes: https://www.amcplus.com/shows/the-walking-dead/episodes--1027559 + * - Movies: https://www.amcplus.com/watch/movies/halloween--1027962 + */ + watchingUrlRegex: /\/(?shows\/.+?\/episodes\/.+)|\/watch\/(?movies\/.+)/, + }); + } +} + +export const AmcPlusParser = new _AmcPlusParser(); diff --git a/src/services/amc-plus/AmcPlusService.ts b/src/services/amc-plus/AmcPlusService.ts new file mode 100644 index 00000000..09305f68 --- /dev/null +++ b/src/services/amc-plus/AmcPlusService.ts @@ -0,0 +1,11 @@ +import { Service } from '@models/Service'; + +export const AmcPlusService = new Service({ + id: 'amc-plus', + name: 'AMC Plus', + homePage: 'https://www.amcplus.com/', + hostPatterns: ['*://*.amcplus.com/*', '*://*.amcn.com/*'], + hasScrobbler: true, + hasSync: false, + hasAutoSync: false, +}); diff --git a/src/services/amc-plus/amc-plus.ts b/src/services/amc-plus/amc-plus.ts new file mode 100644 index 00000000..b9368883 --- /dev/null +++ b/src/services/amc-plus/amc-plus.ts @@ -0,0 +1,4 @@ +import '@/amc-plus/AmcPlusParser'; +import { init } from '@service'; + +void init('amc-plus');