From 30e5ae7121525521a99d52881d51570213eda688 Mon Sep 17 00:00:00 2001 From: frost768 Date: Mon, 29 May 2023 22:10:07 +0300 Subject: [PATCH 01/45] add missing translation keys and polish translation --- src/components/popout/FloatingCard.tsx | 2 +- src/setup/i18n.ts | 4 + src/setup/locales/en/translation.json | 11 +- src/setup/locales/pl/translation.json | 137 ++++++++++++++++++ src/setup/locales/tr/translation.json | 11 +- .../popouts/EpisodeSelectionPopout.tsx | 15 +- 6 files changed, 171 insertions(+), 9 deletions(-) create mode 100644 src/setup/locales/pl/translation.json diff --git a/src/components/popout/FloatingCard.tsx b/src/components/popout/FloatingCard.tsx index b4fd250c..47bfcb59 100644 --- a/src/components/popout/FloatingCard.tsx +++ b/src/components/popout/FloatingCard.tsx @@ -154,7 +154,7 @@ export const FloatingCardView = { className="flex cursor-pointer items-center space-x-2 transition-colors duration-200 hover:text-white" > - Close + {t("videoPlayer.popouts.close")} ); diff --git a/src/setup/i18n.ts b/src/setup/i18n.ts index 6116434a..d3aa261f 100644 --- a/src/setup/i18n.ts +++ b/src/setup/i18n.ts @@ -9,6 +9,7 @@ import en from "./locales/en/translation.json"; import fr from "./locales/fr/translation.json"; import nl from "./locales/nl/translation.json"; import pirate from "./locales/pirate/translation.json"; +import pl from "./locales/pl/translation.json"; import tr from "./locales/tr/translation.json"; import zh from "./locales/zh/translation.json"; @@ -37,6 +38,9 @@ const locales = { pirate: { translation: pirate, }, + pl: { + translation: pl, + }, }; i18n // pass the i18n instance to react-i18next. diff --git a/src/setup/locales/en/translation.json b/src/setup/locales/en/translation.json index d8b81da4..d90b568b 100644 --- a/src/setup/locales/en/translation.json +++ b/src/setup/locales/en/translation.json @@ -71,7 +71,16 @@ "popouts": { "back": "Go back", "sources": "Sources", - "seasons": "Seasons", + "close": "Close", + "seasons": { + "title":"Seasons", + "other": "Other seasons", + "noSeason": "No season" + }, + "episodes": { + "unknown": "Unknown episode", + "noEpisode": "No episode" + }, "captions": "Captions", "playbackSpeed": "Playback speed", "customPlaybackSpeed": "Custom playback speed", diff --git a/src/setup/locales/pl/translation.json b/src/setup/locales/pl/translation.json new file mode 100644 index 00000000..a85d9c70 --- /dev/null +++ b/src/setup/locales/pl/translation.json @@ -0,0 +1,137 @@ +{ + "global": { + "name": "movie-web" + }, + "search": { + "loading_series": "Szukamy twoich ulubionych seriali...", + "loading_movie": "Szukamy twoich ulubionych filmów...", + "loading": "Wczytywanie...", + "allResults": "To wszystko co mamy!", + "noResults": "Nie mogliśmy niczego znaleźć!", + "allFailed": "Nie udało się znaleźć mediów, Spróbuj ponownie!", + "headingTitle": "Wyniki wyszukiwania", + "bookmarks": "Zakładki", + "continueWatching": "Kontynuuj oglądanie", + "title": "Co chciałbyś obejrzeć?", + "placeholder": "Co chciałbyś obejrzeć?" + }, + "media": { + "movie": "Film", + "series": "Serial", + "stopEditing": "Zatrzymaj edycje", + "errors": { + "genericTitle": "Ups, popsuło się!", + "failedMeta": "Nie udało się wczytać metadanych", + "mediaFailed": "Nie udało nam się zarządać mediów, sprawdź połączenie sieciowe i spróbuj ponownie.", + "videoFailed": "Napotkaliśmy błąd podczas odtwarzania rządanego video. Jeśli problem będzie się powtarzać prosimy o zgłoszenie problemu na <0>Serwer Discord lub na <1>GitHub." + } + }, + "seasons": { + "seasonAndEpisode": "S{{season}} E{{episode}}" + }, + "notFound": { + "genericTitle": "Nie znaleziono", + "backArrow": "Wróć na stronę główną", + "media": { + "title": "Nie można znaleźć multimediów", + "description": "Nie mogliśmy znaleźć rządanych multimediów. Albo zostały usunięte, albo grzebałeś przy adresie URL." + }, + "provider": { + "title": "Ten dostawca został wyłączony", + "description": "Mieliśmy problemy z tym dostawcą, albo był zbyt niestabilny, więc musieliśmy go wyłączyć." + }, + "page": { + "title": "Nie można znaleźć tej strony", + "description": "Szukaliśmy wszędzie: w koszu, w szafie a nawet w piwnicy, ale nie byliśmy w stanie znaleźć strony której szukasz." + } + }, + "searchBar": { + "movie": "Filmy", + "series": "Seriale", + "Search": "Szukaj" + }, + "videoPlayer": { + "findingBestVideo": "Szukamy najlepszego video dla ciebie", + "noVideos": "Oj, Nie mogliśmy znaleźć żadnego video", + "loading": "Wczytywanie...", + "backToHome": "Wróć na stronę główną", + "backToHomeShort": "Wróć", + "seasonAndEpisode": "S{{season}} E{{episode}}", + "timeLeft": "Pozostało {{timeLeft}}", + "finishAt": "Zakończ na {{timeFinished, datetime}}", + "buttons": { + "episodes": "Odcinki", + "source": "Źródło", + "captions": "Napisy", + "download": "Pobierz", + "settings": "Ustawienia", + "pictureInPicture": "Obraz w obrazie (PIP)", + "playbackSpeed": "Prędkość odtwarzania" + }, + "popouts": { + "close": "Zamknąć", + "seasons": { + "title":"Sezony", + "other": "Inne sezony", + "noSeason": "Brak sezonu" + }, + "episodes": { + "unknown": "Nieznany odcinki", + "noEpisode": "Brak odcinki" + }, + "back": "Wróć", + "sources": "Źródła", + "captions": "Napisy", + "playbackSpeed": "Prędkość odtwarzania", + "customPlaybackSpeed": "Niestandardowa prędkość odtwarzania", + "captionPreferences": { + "title": "Personalizuj", + "delay": "Opóźnienie", + "fontSize": "Rozmiar", + "opacity": "Przeźroczystość", + "color": "Kolor" + }, + "episode": "E{{index}} - {{title}}", + "noCaptions": "Brak napisów", + "linkedCaptions": "Załączone napisy", + "customCaption": "Napisy niestandardowe", + "uploadCustomCaption": "Załącz", + "noEmbeds": "Nie znaleziono osadzonych mediów dla tego źródła", + + "errors": { + "loadingWentWong": "Coś poszło nie tak {{seasonTitle}}", + "embedsError": "Coś poszło nie tak przy wczytywaniu osadzonych mediów" + }, + "descriptions": { + "sources": "Którego dostawcy chciałbyś używać?", + "embeds": "Wybierz, które video chcesz zobaczyć", + "seasons": "Wybierz, który sezon chcesz obejrzeć", + "episode": "Wybierz odcinek", + "captions": "Zmień język napisów", + "captionPreferences": "Ustaw napisy, tak jak ci to odpowiada", + "playbackSpeed": "Zmień prędkość odtwarzania" + } + }, + "errors": { + "fatalError": "Odtwarzacz napotkał poważny błąd, Prosimy o złoszenie tego na <0>Serwer Discord lub na <1>GitHub." + } + }, + "settings": { + "title": "Ustawienia", + "language": "Język", + "captionLanguage": "Język napisów" + }, + "v3": { + "newSiteTitle": "Nowa wersja została wydana!", + "newDomain": "https://movie-web.app", + "newDomainText": "movie-web przeniesie się wkrótce na nowy adres: <0>https://movie-web.app. Prosimy zaaktualizować swoje zakładki ponieważ <1>stara strona przestanie działać {{date}}.", + "tireless": "Pracowaliśmy niestrudzenie nad tą aktualizacją, Mamy nadzieję że będziecie zadowoleni z tego nad czym pracowaliśmy przez ostatnie parę miesięcy.", + "leaveAnnouncement": "Zabierz mnie tam!" + }, + "casting": { + "casting": "Przesyłanie do urządzenia..." + }, + "errors": { + "offline": "Sprawdź swoje połączenie sieciowe" + } +} diff --git a/src/setup/locales/tr/translation.json b/src/setup/locales/tr/translation.json index 326cc35d..bab6cd1d 100644 --- a/src/setup/locales/tr/translation.json +++ b/src/setup/locales/tr/translation.json @@ -71,7 +71,16 @@ "popouts": { "back": "Geri git", "sources": "Kaynaklar", - "seasons": "Sezonlar", + "close":"Kapat", + "seasons": { + "title":"Sezonlar", + "other": "Diğer sezonlar", + "noSeason": "Sezon yok" + }, + "episodes": { + "unknown": "Bilinmeyen bölüm", + "noEpisode": "Bölüm yok" + }, "captions": "Altyazılar", "playbackSpeed": "Oynatma hızı", "customPlaybackSpeed": "Özel oynatma hızı", diff --git a/src/video/components/popouts/EpisodeSelectionPopout.tsx b/src/video/components/popouts/EpisodeSelectionPopout.tsx index bd152378..c80045bd 100644 --- a/src/video/components/popouts/EpisodeSelectionPopout.tsx +++ b/src/video/components/popouts/EpisodeSelectionPopout.tsx @@ -99,10 +99,10 @@ export function EpisodeSelectionPopout() { <> navigate("/episodes")} - backText={`To ${currentSeasonInfo?.title.toLowerCase()}`} + backText={currentSeasonInfo?.title} /> {currentSeasonInfo @@ -115,12 +115,15 @@ export function EpisodeSelectionPopout() { {season.title} )) - : "No season"} + : t("videoPlayer.popouts.seasons.noSeason")} navigate("/episodes/seasons")} className="flex cursor-pointer items-center space-x-2 transition-colors duration-200 hover:text-white" > - Other seasons + {t("videoPlayer.popouts.seasons.other")} } @@ -181,7 +184,7 @@ export function EpisodeSelectionPopout() { })} )) - : "No episodes"} + : t("videoPlayer.popouts.episodes.noEpisode")} )} From cc4f64032a92f82170de9e687c70e66f1bd17d96 Mon Sep 17 00:00:00 2001 From: Federico Benedetti Date: Sat, 3 Jun 2023 11:55:57 +0200 Subject: [PATCH 02/45] Add Italian language support --- src/setup/i18n.ts | 4 + src/setup/locales/it/translation.json | 128 ++++++++++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 src/setup/locales/it/translation.json diff --git a/src/setup/i18n.ts b/src/setup/i18n.ts index 6116434a..aa69517a 100644 --- a/src/setup/i18n.ts +++ b/src/setup/i18n.ts @@ -11,11 +11,15 @@ import nl from "./locales/nl/translation.json"; import pirate from "./locales/pirate/translation.json"; import tr from "./locales/tr/translation.json"; import zh from "./locales/zh/translation.json"; +import it from "./locales/it/translation.json"; const locales = { en: { translation: en, }, + it: { + translation: it, + }, nl: { translation: nl, }, diff --git a/src/setup/locales/it/translation.json b/src/setup/locales/it/translation.json new file mode 100644 index 00000000..7c28992c --- /dev/null +++ b/src/setup/locales/it/translation.json @@ -0,0 +1,128 @@ +{ + "global": { + "name": "movie-web" + }, + "search": { + "loading_series": "Recupero delle tue serie preferite...", + "loading_movie": "Recupero dei tuoi film preferiti...", + "loading": "Caricamento...", + "allResults": "Ecco tutto ciò che abbiamo!", + "noResults": "Non abbiamo trovato nulla!", + "allFailed": "Impossibile trovare i media, riprova!", + "headingTitle": "Risultati della ricerca", + "bookmarks": "Segnalibri", + "continueWatching": "Continua a guardare", + "title": "Cosa vuoi guardare?", + "placeholder": "Cosa vuoi guardare?" + }, + "media": { + "movie": "Film", + "series": "Serie", + "stopEditing": "Interrompi modifica", + "errors": { + "genericTitle": "Ops, qualcosa si è rotto!", + "failedMeta": "Caricamento dei metadati non riuscito", + "mediaFailed": "Impossibile richiedere il media che hai richiesto, controlla la tua connessione internet e riprova.", + "videoFailed": "Si è verificato un errore durante la riproduzione del video che hai richiesto. Se ciò continua a accadere, segnala il problema sul <0>server Discord o su <1>GitHub." + } + }, + "seasons": { + "seasonAndEpisode": "S{{season}} E{{episode}}" + }, + "notFound": { + "genericTitle": "Non trovato", + "backArrow": "Torna alla home", + "media": { + "title": "Impossibile trovare quel media", + "description": "Non siamo riusciti a trovare il media richiesto. È stato rimosso o hai manomesso l'URL." + }, + "provider": { + "title": "Questo provider è stato disabilitato", + "description": "Abbiamo riscontrato problemi con il provider o era troppo instabile da utilizzare, quindi abbiamo dovuto disabilitarlo." + }, + "page": { + "title": "Impossibile trovare quella pagina", + "description": "Abbiamo cercato ovunque: sotto i bidoni, nell'armadio, dietro il proxy, ma alla fine non siamo riusciti a trovare la pagina che stai cercando." + } + }, + "searchBar": { + "movie": "Film", + "series": "Serie", + "Search": "Cerca" + }, + "videoPlayer": { + "findingBestVideo": "Ricerca del miglior video per te", + "noVideos": "Ops, non è stato possibile trovare alcun video per te", + "loading": "Caricamento...", + "backToHome": "Torna alla home", + "backToHomeShort": "Indietro", + "seasonAndEpisode": "S{{season}} E{{episode}}", + "timeLeft": "{{timeLeft}} rimanente", + "finishAt": "Fine alle {{timeFinished, datetime}}", + "buttons": { + "episodes": "Episodi", + "source": "Fonte", + "captions": "Sottotitoli", + "download": "Download", + "settings": "Impostazioni", + "pictureInPicture": "Picture in Picture", + "playbackSpeed": "Velocità di riproduzione" + }, + "popouts": { + "back": "Torna indietro", + "sources": "Fonti", + "seasons": "Stagioni", + "captions": "Sottotitoli", + "playbackSpeed": "Velocità di riproduzione", + "customPlaybackSpeed": "Velocità di riproduzione personalizzata", + "captionPreferences": { + "title": "Personalizza", + "delay": "Ritardo", + "fontSize": "Dimensione carattere", + "opacity": "Opacità", + "color": "Colore" + }, + "episode": "E{{index}} - {{title}}", + "noCaptions": "Nessun sottotitolo", + "linkedCaptions": "Sottotitoli collegati", + "customCaption": "Sottotitolo personalizzato", + "uploadCustomCaption": "Carica sottotitolo", + "noEmbeds": "Nessun embed è stato trovato per questa fonte", + + "errors": { + "loadingWentWong": "Si è verificato un problema durante il caricamento degli episodi per {{seasonTitle}}", + "embedsError": "Si è verificato un problema durante il caricamento degli embed per questa cosa che ti piace" + }, + "descriptions": { + "sources": "Quale provider desideri utilizzare?", + "embeds": "Scegli quale video visualizzare", + "seasons": "Scegli quale stagione vuoi guardare", + "episode": "Scegli un episodio", + "captions": "Scegli una lingua per i sottotitoli", + "captionPreferences": "Personalizza l'aspetto dei sottotitoli", + "playbackSpeed": "Cambia la velocità di riproduzione" + } + }, + "errors": { + "fatalError": "Il lettore video ha riscontrato un errore fatale, segnalalo sul <0>server Discord o su <1>GitHub." + } + }, + "settings": { + "title": "Impostazioni", + "language": "Lingua", + "captionLanguage": "Lingua dei sottotitoli" + }, + "v3": { + "newSiteTitle": "Nuova versione ora disponibile!", + "newDomain": "https://movie-web.app", + "newDomainText": "movie-web si sposterà presto su un nuovo dominio: <0>https://movie-web.app. Assicurati di aggiornare tutti i tuoi segnalibri poiché <1>il vecchio sito smetterà di funzionare il {{date}}.", + "tireless": "Abbiamo lavorato instancabilmente su questo nuovo aggiornamento, speriamo che ti piaccia quello su cui abbiamo lavorato negli ultimi mesi.", + "leaveAnnouncement": "Portami lì!" + }, + "casting": { + "casting": "Trasmissione su dispositivo in corso..." + }, + "errors": { + "offline": "Controlla la tua connessione internet" + } +} From b7033a31c453f6cfe70117db5042c1a0c645f9a0 Mon Sep 17 00:00:00 2001 From: Federico Benedetti Date: Sat, 3 Jun 2023 12:15:19 +0200 Subject: [PATCH 03/45] Fix locale import position --- src/setup/i18n.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/setup/i18n.ts b/src/setup/i18n.ts index aa69517a..e5f9358f 100644 --- a/src/setup/i18n.ts +++ b/src/setup/i18n.ts @@ -7,11 +7,11 @@ import cs from "./locales/cs/translation.json"; import de from "./locales/de/translation.json"; import en from "./locales/en/translation.json"; import fr from "./locales/fr/translation.json"; +import it from "./locales/it/translation.json"; import nl from "./locales/nl/translation.json"; import pirate from "./locales/pirate/translation.json"; import tr from "./locales/tr/translation.json"; import zh from "./locales/zh/translation.json"; -import it from "./locales/it/translation.json"; const locales = { en: { From 18bde24b3a21bd2c3df793a76fd072e476febf0f Mon Sep 17 00:00:00 2001 From: cloud <62519659+lem6ns@users.noreply.github.com> Date: Sun, 11 Jun 2023 11:29:55 -0600 Subject: [PATCH 04/45] feat(provider): Remote Stream --- src/backend/index.ts | 1 + src/backend/providers/remotestream.ts | 49 +++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 src/backend/providers/remotestream.ts diff --git a/src/backend/index.ts b/src/backend/index.ts index eb0ad897..d62f557b 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -9,6 +9,7 @@ import "./providers/m4ufree"; import "./providers/hdwatched"; import "./providers/2embed"; import "./providers/sflix"; +import "./providers/remotestream"; // embeds import "./embeds/streamm4u"; diff --git a/src/backend/providers/remotestream.ts b/src/backend/providers/remotestream.ts new file mode 100644 index 00000000..cf19f826 --- /dev/null +++ b/src/backend/providers/remotestream.ts @@ -0,0 +1,49 @@ +import { mwFetch } from "@/backend/helpers/fetch"; +import { registerProvider } from "@/backend/helpers/register"; +import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams"; +import { MWMediaType } from "@/backend/metadata/types"; + +const remotestreamBase = `https://fsa.remotestre.am`; + +registerProvider({ + id: "remotestream", + displayName: "Remote Stream", + disabled: false, + rank: 50, + type: [MWMediaType.MOVIE, MWMediaType.SERIES], + + async scrape({ media, episode, progress }) { + if (!this.type.includes(media.meta.type)) { + throw new Error("Unsupported type"); + } + + progress(30); + const type = media.meta.type === MWMediaType.MOVIE ? "Movies" : "Shows"; + let playlistLink = `${remotestreamBase}/${type}/${media.tmdbId}`; + + if (media.meta.type === MWMediaType.SERIES) { + const seasonNumber = media.meta.seasonData.number; + const episodeNumber = media.meta.seasonData.episodes.find( + (e) => e.id === episode + )?.number; + + playlistLink += `/${seasonNumber}/${episodeNumber}.m3u8`; + } else { + playlistLink += `/${media.tmdbId}.m3u8`; + } + + const streamRes = await mwFetch(playlistLink); + if (streamRes.type !== "application/x-mpegurl") + throw new Error("No watchable item found"); + progress(90); + return { + embeds: [], + stream: { + streamUrl: playlistLink, + quality: MWStreamQuality.QUNKNOWN, + type: MWStreamType.HLS, + captions: [], + }, + }; + }, +}); From 893a385f0043a989cbb39ab1772737294c49ca9b Mon Sep 17 00:00:00 2001 From: cloud <62519659+lem6ns@users.noreply.github.com> Date: Sun, 11 Jun 2023 11:34:57 -0600 Subject: [PATCH 05/45] fix(remotestream): additional path for tv --- src/backend/providers/remotestream.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/providers/remotestream.ts b/src/backend/providers/remotestream.ts index cf19f826..8439d2c1 100644 --- a/src/backend/providers/remotestream.ts +++ b/src/backend/providers/remotestream.ts @@ -27,7 +27,7 @@ registerProvider({ (e) => e.id === episode )?.number; - playlistLink += `/${seasonNumber}/${episodeNumber}.m3u8`; + playlistLink += `/${seasonNumber}/${episodeNumber}/${episodeNumber}.m3u8`; } else { playlistLink += `/${media.tmdbId}.m3u8`; } From ef782974fe20115e2993e482f1a7a48f8087b287 Mon Sep 17 00:00:00 2001 From: cloud <62519659+lem6ns@users.noreply.github.com> Date: Sun, 11 Jun 2023 11:36:05 -0600 Subject: [PATCH 06/45] fix(remotestream): Duplicate rank number --- src/backend/providers/remotestream.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/providers/remotestream.ts b/src/backend/providers/remotestream.ts index 8439d2c1..02c0f199 100644 --- a/src/backend/providers/remotestream.ts +++ b/src/backend/providers/remotestream.ts @@ -9,7 +9,7 @@ registerProvider({ id: "remotestream", displayName: "Remote Stream", disabled: false, - rank: 50, + rank: 55, type: [MWMediaType.MOVIE, MWMediaType.SERIES], async scrape({ media, episode, progress }) { From 1a613287f8453672c83d9adb0fdbbb77d0f7772b Mon Sep 17 00:00:00 2001 From: cloud <62519659+lem6ns@users.noreply.github.com> Date: Sun, 11 Jun 2023 10:44:51 -0600 Subject: [PATCH 07/45] feat(provider): streamflix --- src/backend/index.ts | 1 + src/backend/providers/streamflix.ts | 70 +++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 src/backend/providers/streamflix.ts diff --git a/src/backend/index.ts b/src/backend/index.ts index eb0ad897..447c2165 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -9,6 +9,7 @@ import "./providers/m4ufree"; import "./providers/hdwatched"; import "./providers/2embed"; import "./providers/sflix"; +import "./providers/streamflix"; // embeds import "./embeds/streamm4u"; diff --git a/src/backend/providers/streamflix.ts b/src/backend/providers/streamflix.ts new file mode 100644 index 00000000..90dd4975 --- /dev/null +++ b/src/backend/providers/streamflix.ts @@ -0,0 +1,70 @@ +import { proxiedFetch } from "@/backend/helpers/fetch"; +import { registerProvider } from "@/backend/helpers/register"; +import { + MWCaptionType, + MWStreamQuality, + MWStreamType, +} from "@/backend/helpers/streams"; +import { MWMediaType } from "@/backend/metadata/types"; + +const streamflixBase = "https://us-west2-compute-proxied.streamflix.one"; + +const qualityMap: Record = { + 360: MWStreamQuality.Q360P, + 540: MWStreamQuality.Q540P, + 480: MWStreamQuality.Q480P, + 720: MWStreamQuality.Q720P, + 1080: MWStreamQuality.Q1080P, +}; + +registerProvider({ + id: "streamflix", + displayName: "StreamFlix", + disabled: false, + rank: 69, + type: [MWMediaType.MOVIE, MWMediaType.SERIES], + + async scrape({ media, episode, progress }) { + if (!this.type.includes(media.meta.type)) { + throw new Error("Unsupported type"); + } + + progress(30); + const type = media.meta.type === MWMediaType.MOVIE ? "movies" : "tv"; + let seasonNumber: number | undefined; + let episodeNumber: number | undefined; + + if (media.meta.type === MWMediaType.SERIES) { + // can't do type === "tv" here :( + seasonNumber = media.meta.seasonData.number; + episodeNumber = media.meta.seasonData.episodes.find( + (e: any) => e.id === episode + )?.number; + } + + const streamRes = await proxiedFetch(`/api/player/${type}`, { + baseURL: streamflixBase, + params: { + id: media.tmdbId, + s: seasonNumber, + e: episodeNumber, + }, + }); + if (!streamRes.headers.Referer) throw new Error("No watchable item found"); + progress(90); + return { + embeds: [], + stream: { + streamUrl: streamRes.sources[0].url, + quality: qualityMap[streamRes.sources[0].quality], + type: MWStreamType.HLS, + captions: streamRes.subtitles.map((s: Record) => ({ + needsProxy: true, + url: s.url, + type: MWCaptionType.VTT, + langIso: s.lang, + })), + }, + }; + }, +}); From 424ee6fe7791b356ad43765d747dfd6420664f6e Mon Sep 17 00:00:00 2001 From: spinixster <135334442+spinixster@users.noreply.github.com> Date: Thu, 15 Jun 2023 08:55:40 +0700 Subject: [PATCH 08/45] Update i18n.ts --- src/setup/i18n.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/setup/i18n.ts b/src/setup/i18n.ts index 6116434a..a225c91e 100644 --- a/src/setup/i18n.ts +++ b/src/setup/i18n.ts @@ -11,6 +11,7 @@ import nl from "./locales/nl/translation.json"; import pirate from "./locales/pirate/translation.json"; import tr from "./locales/tr/translation.json"; import zh from "./locales/zh/translation.json"; +import vi from "./locales/vi/translation.json"; const locales = { en: { From f9d756e0efe4321e53e30c9b8ba7df88570d8d1a Mon Sep 17 00:00:00 2001 From: spinixster <135334442+spinixster@users.noreply.github.com> Date: Thu, 15 Jun 2023 09:06:19 +0700 Subject: [PATCH 09/45] Update i18n.ts --- src/setup/i18n.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/setup/i18n.ts b/src/setup/i18n.ts index a225c91e..21388b9f 100644 --- a/src/setup/i18n.ts +++ b/src/setup/i18n.ts @@ -10,8 +10,8 @@ import fr from "./locales/fr/translation.json"; import nl from "./locales/nl/translation.json"; import pirate from "./locales/pirate/translation.json"; import tr from "./locales/tr/translation.json"; -import zh from "./locales/zh/translation.json"; import vi from "./locales/vi/translation.json"; +import zh from "./locales/zh/translation.json"; const locales = { en: { From db75f2320d78c721d3754e1162ad49497d6a1a79 Mon Sep 17 00:00:00 2001 From: spinixster <135334442+spinixster@users.noreply.github.com> Date: Thu, 15 Jun 2023 10:46:05 +0700 Subject: [PATCH 10/45] Add files via upload add translation --- src/setup/locales/translation.json | 128 +++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 src/setup/locales/translation.json diff --git a/src/setup/locales/translation.json b/src/setup/locales/translation.json new file mode 100644 index 00000000..dae9c21d --- /dev/null +++ b/src/setup/locales/translation.json @@ -0,0 +1,128 @@ +{ + "global": { + "name": "movie-web" + }, + "search": { + "loading_series": "Đang tìm chương trình yêu thích của bạn...", + "loading_movie": "Đang tìm bộ phim yêu thích của bạn...", + "loading": "Đang tải...", + "allResults": "Đó là tất cả chúng tối có!", + "noResults": "Chúng tôi không thể tìm thấy gì!", + "allFailed": "Không thể tìm thấy nội dung, hãy thử lại!", + "headingTitle": "Kết quả tìm kiếm", + "bookmarks": "Đánh dấu", + "continueWatching": "Tiếp tục xem", + "title": "Bạn muốn xem gì?", + "placeholder": "Bạn muốn xem gì?" + }, + "media": { + "movie": "Phim", + "series": "Chương trình truyền hình", + "stopEditing": "Hãy dừng chỉnh sửa", + "errors": { + "genericTitle": "Rất tiếc, nó đã hỏng!", + "failedMeta": "Không thể tải meta", + "mediaFailed": "Chúng tôi không thể tìm thấy nội dung mà bạn yêu cầu, hãy kiểm tra kết nối internet của bạn và thử lại.", + "videoFailed": "Chúng tôi đã gặp lỗi khi phát nội dung mà bạn yêu cầu. Nếu điều này tiếp tục xảy ra, vui lòng báo cáo sự cố trên <0>máy chủ Discord hoặc trên <1>GitHub." + } + }, + "seasons": { + "seasonAndEpisode": "M{{season}} T{{episode}}" + }, + "notFound": { + "genericTitle": "Không tìm thấy", + "backArrow": "Quay lại trang chính", + "media": { + "title": "Không thể tìm thấy nội dung", + "description": "Chúng tôi không thể tìm thấy nội dung mà bạn yêu cầu. Hoặc là nó đã bị xóa, hoặc bạn đã xáo trộn URL" + }, + "provider": { + "title": "Nhà cung cấp này đã bị vô hiệu hóa", + "description": "Chúng tôi gặp vấn đề với nhà cung cấp hoặc nó quá bất ổn để sử dụng, cho nên chúng tôi đã phải vô hiệu hóa nó." + }, + "page": { + "title": "Không thể tìm thấy trang", + "description": "Chúng tôi đã tìm kiếm khắp nơi: dưới thùng rác, trong tủ quần áo, đằng sau máy chủ proxy nhưng vẫn không thể tìm thấy trang bạn đang tìm kiếm." + } + }, + "searchBar": { + "movie": "Phim", + "series": "Chương trình truyền hình", + "Search": "Tìm kiếm" + }, + "videoPlayer": { + "findingBestVideo": "Đang tìm nội dung tốt nhất cho bạn", + "noVideos": "Rất tiếc, không tìm thấy nội dung nào cho bạn", + "loading": "Đang tải...", + "backToHome": "Quay lại trang chính", + "backToHomeShort": "Quay lại", + "seasonAndEpisode": "M{{season}} T{{episode}}", + "timeLeft": "Còn {{timeLeft}}", + "finishAt": "Kết thúc vào {{timeFinished, datetime}}", + "buttons": { + "episodes": "Tập", + "source": "Source", + "captions": "Phụ đề", + "download": "Tải xuống", + "settings": "Cài đặt", + "pictureInPicture": "Hình trong hình", + "playbackSpeed": "Tốc độ phát" + }, + "popouts": { + "back": "Quay lại", + "sources": "Nguồn", + "seasons": "Mùa", + "captions": "Phụ đề", + "playbackSpeed": "Tốc độ phát", + "customPlaybackSpeed": "Tủy chỉnh tốc độ phát", + "captionPreferences": { + "title": "Tùy chỉnh", + "delay": "Trì hoãn", + "fontSize": "Kích cỡ", + "opacity": "Độ mờ", + "color": "Màu sắc" + }, + "episode": "T{{index}} - {{title}}", + "noCaptions": "Không phụ đề", + "linkedCaptions": "Phụ đề được liên kết", + "customCaption": "Phụ đề tùy chỉnh", + "uploadCustomCaption": "Tải phụ đề lên", + "noEmbeds": "Không tìm thấy nội dung nhúng nào cho nguồn này", + + "errors": { + "loadingWentWong": "Đã xảy ra lỗi khi tải các tập phim cho {{seasonTitle}}", + "embedsError": "Đã xảy ra lỗi khi tải nội dung nhúng cho nội dung bạn thích này" + }, + "descriptions": { + "sources": "Bạn muốn sử dụng nhà cung cấp nào?", + "embeds": "Chọn video để xem", + "seasons": "Chọn mùa bạn muốn xem", + "episode": "Chọn một tập", + "captions": "Chọn ngôn ngữ của phụ đề", + "captionPreferences": "Làm cho phụ đề trông như thế nào bạn muốn", + "playbackSpeed": "Thay đổi tốc độ phát" + } + }, + "errors": { + "fatalError": "Trình phát video đã gặp phải lỗi nghiêm trọng, vui lòng báo cáo sự cố trên <0>máy chủ Discord hoặc trên <1>GitHub." + } + }, + "settings": { + "title": "Cài đặt", + "language": "Ngôn ngữ", + "captionLanguage": "Ngôn ngữ phụ đề" + }, + "v3": { + "newSiteTitle": "Phiên bản mới đã được phát hành!", + "newDomain": "https://movie-web.app", + "newDomainText": "movie-web sẽ sớm chuyển sang trang mới: <0>https://movie-web.app. Hãy đảm bảo rằng các đánh dấu đã được cập nhật vì <1>trang web cũ sẽ dừng hoạt động vào {{date}}.", + "tireless": "Chúng tôi đã làm việc vất vả để tạo phiên bản mới này, chúng tôi hy vọng bạn sẽ thích những gì chúng tôi đã nung nấu trong những tháng qua.", + "leaveAnnouncement": "Hãy đưa tôi đến đó!" + }, + "casting": { + "casting": "Đang truyền tới thiết bị..." + }, + "errors": { + "offline": "Hãy kiểm tra kết nối Internet của bạn" + } +} From 80dd2158dffbc9d33e154971bd44d828b5a61b38 Mon Sep 17 00:00:00 2001 From: spinixster <135334442+spinixster@users.noreply.github.com> Date: Thu, 15 Jun 2023 10:48:26 +0700 Subject: [PATCH 11/45] Create translation.json --- src/setup/locales/vi/translation.json | 128 ++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 src/setup/locales/vi/translation.json diff --git a/src/setup/locales/vi/translation.json b/src/setup/locales/vi/translation.json new file mode 100644 index 00000000..45494388 --- /dev/null +++ b/src/setup/locales/vi/translation.json @@ -0,0 +1,128 @@ +{ + "global": { + "name": "movie-web" + }, + "search": { + "loading_series": "Đang tìm chương trình yêu thích của bạn...", + "loading_movie": "Đang tìm bộ phim yêu thích của bạn...", + "loading": "Đang tải...", + "allResults": "Đó là tất cả chúng tôi có!", + "noResults": "Chúng tôi không thể tìm thấy gì!", + "allFailed": "Không thể tìm thấy nội dung, hãy thử lại!", + "headingTitle": "Kết quả tìm kiếm", + "bookmarks": "Đánh dấu", + "continueWatching": "Tiếp tục xem", + "title": "Bạn muốn xem gì?", + "placeholder": "Bạn muốn xem gì?" + }, + "media": { + "movie": "Phim", + "series": "Chương trình truyền hình", + "stopEditing": "Hãy dừng chỉnh sửa", + "errors": { + "genericTitle": "Rất tiếc, nó đã hỏng!", + "failedMeta": "Không thể tải meta", + "mediaFailed": "Chúng tôi không thể tìm thấy nội dung mà bạn yêu cầu, hãy kiểm tra kết nối internet của bạn và thử lại.", + "videoFailed": "Chúng tôi đã gặp lỗi khi phát nội dung mà bạn yêu cầu. Nếu điều này tiếp tục xảy ra, vui lòng báo cáo sự cố trên <0>máy chủ Discord hoặc trên <1>GitHub." + } + }, + "seasons": { + "seasonAndEpisode": "M{{season}} T{{episode}}" + }, + "notFound": { + "genericTitle": "Không tìm thấy", + "backArrow": "Quay lại trang chính", + "media": { + "title": "Không thể tìm thấy nội dung", + "description": "Chúng tôi không thể tìm thấy nội dung mà bạn yêu cầu. Hoặc là nó đã bị xóa, hoặc bạn đã xáo trộn URL" + }, + "provider": { + "title": "Nhà cung cấp này đã bị vô hiệu hóa", + "description": "Chúng tôi đã gặp vấn đề với nhà cung cấp hoặc nó quá bất ổn để sử dụng, cho nên chúng tôi đã phải vô hiệu hóa nó." + }, + "page": { + "title": "Không thể tìm thấy trang", + "description": "Chúng tôi đã tìm kiếm khắp nơi: dưới thùng rác, trong tủ quần áo, đằng sau máy chủ proxy nhưng vẫn không thể tìm thấy trang bạn đang tìm kiếm." + } + }, + "searchBar": { + "movie": "Phim", + "series": "Chương trình truyền hình", + "Search": "Tìm kiếm" + }, + "videoPlayer": { + "findingBestVideo": "Đang tìm nội dung tốt nhất cho bạn", + "noVideos": "Rất tiếc, không tìm thấy nội dung nào cho bạn", + "loading": "Đang tải...", + "backToHome": "Quay lại trang chính", + "backToHomeShort": "Quay lại", + "seasonAndEpisode": "M{{season}} T{{episode}}", + "timeLeft": "Còn {{timeLeft}}", + "finishAt": "Kết thúc vào {{timeFinished, datetime}}", + "buttons": { + "episodes": "Tập", + "source": "Source", + "captions": "Phụ đề", + "download": "Tải xuống", + "settings": "Cài đặt", + "pictureInPicture": "Hình trong hình", + "playbackSpeed": "Tốc độ phát" + }, + "popouts": { + "back": "Quay lại", + "sources": "Nguồn", + "seasons": "Mùa", + "captions": "Phụ đề", + "playbackSpeed": "Tốc độ phát", + "customPlaybackSpeed": "Tủy chỉnh tốc độ phát", + "captionPreferences": { + "title": "Tùy chỉnh", + "delay": "Trì hoãn", + "fontSize": "Kích cỡ", + "opacity": "Độ mờ", + "color": "Màu sắc" + }, + "episode": "T{{index}} - {{title}}", + "noCaptions": "Không phụ đề", + "linkedCaptions": "Phụ đề được liên kết", + "customCaption": "Phụ đề tùy chỉnh", + "uploadCustomCaption": "Tải phụ đề lên", + "noEmbeds": "Không tìm thấy nội dung nhúng nào cho nguồn này", + + "errors": { + "loadingWentWong": "Đã xảy ra lỗi khi tải các tập phim cho {{seasonTitle}}", + "embedsError": "Đã xảy ra lỗi khi tải nội dung nhúng cho nội dung bạn thích này" + }, + "descriptions": { + "sources": "Bạn muốn sử dụng nhà cung cấp nào?", + "embeds": "Chọn video để xem", + "seasons": "Chọn mùa bạn muốn xem", + "episode": "Chọn một tập", + "captions": "Chọn ngôn ngữ của phụ đề", + "captionPreferences": "Làm cho phụ đề trông như thế nào bạn muốn", + "playbackSpeed": "Thay đổi tốc độ phát" + } + }, + "errors": { + "fatalError": "Trình phát video đã gặp phải lỗi nghiêm trọng, vui lòng báo cáo sự cố trên <0>máy chủ Discord hoặc trên <1>GitHub." + } + }, + "settings": { + "title": "Cài đặt", + "language": "Ngôn ngữ", + "captionLanguage": "Ngôn ngữ phụ đề" + }, + "v3": { + "newSiteTitle": "Phiên bản mới đã được phát hành!", + "newDomain": "https://movie-web.app", + "newDomainText": "movie-web sẽ sớm chuyển sang trang mới: <0>https://movie-web.app. Hãy đảm bảo rằng các đánh dấu đã được cập nhật vì <1>trang web cũ sẽ dừng hoạt động vào {{date}}.", + "tireless": "Chúng tôi đã làm việc vất vả để tạo phiên bản mới này, chúng tôi hy vọng bạn sẽ thích những gì chúng tôi đã nung nấu trong những tháng qua.", + "leaveAnnouncement": "Hãy đưa tôi đến đó!" + }, + "casting": { + "casting": "Đang truyền tới thiết bị..." + }, + "errors": { + "offline": "Hãy kiểm tra kết nối Internet của bạn" + } +} From 61c3ed076f778549a34f3ec92dfbe4dde65951dc Mon Sep 17 00:00:00 2001 From: spinixster <135334442+spinixster@users.noreply.github.com> Date: Thu, 15 Jun 2023 10:48:45 +0700 Subject: [PATCH 12/45] Delete translation.json --- src/setup/locales/translation.json | 128 ----------------------------- 1 file changed, 128 deletions(-) delete mode 100644 src/setup/locales/translation.json diff --git a/src/setup/locales/translation.json b/src/setup/locales/translation.json deleted file mode 100644 index dae9c21d..00000000 --- a/src/setup/locales/translation.json +++ /dev/null @@ -1,128 +0,0 @@ -{ - "global": { - "name": "movie-web" - }, - "search": { - "loading_series": "Đang tìm chương trình yêu thích của bạn...", - "loading_movie": "Đang tìm bộ phim yêu thích của bạn...", - "loading": "Đang tải...", - "allResults": "Đó là tất cả chúng tối có!", - "noResults": "Chúng tôi không thể tìm thấy gì!", - "allFailed": "Không thể tìm thấy nội dung, hãy thử lại!", - "headingTitle": "Kết quả tìm kiếm", - "bookmarks": "Đánh dấu", - "continueWatching": "Tiếp tục xem", - "title": "Bạn muốn xem gì?", - "placeholder": "Bạn muốn xem gì?" - }, - "media": { - "movie": "Phim", - "series": "Chương trình truyền hình", - "stopEditing": "Hãy dừng chỉnh sửa", - "errors": { - "genericTitle": "Rất tiếc, nó đã hỏng!", - "failedMeta": "Không thể tải meta", - "mediaFailed": "Chúng tôi không thể tìm thấy nội dung mà bạn yêu cầu, hãy kiểm tra kết nối internet của bạn và thử lại.", - "videoFailed": "Chúng tôi đã gặp lỗi khi phát nội dung mà bạn yêu cầu. Nếu điều này tiếp tục xảy ra, vui lòng báo cáo sự cố trên <0>máy chủ Discord hoặc trên <1>GitHub." - } - }, - "seasons": { - "seasonAndEpisode": "M{{season}} T{{episode}}" - }, - "notFound": { - "genericTitle": "Không tìm thấy", - "backArrow": "Quay lại trang chính", - "media": { - "title": "Không thể tìm thấy nội dung", - "description": "Chúng tôi không thể tìm thấy nội dung mà bạn yêu cầu. Hoặc là nó đã bị xóa, hoặc bạn đã xáo trộn URL" - }, - "provider": { - "title": "Nhà cung cấp này đã bị vô hiệu hóa", - "description": "Chúng tôi gặp vấn đề với nhà cung cấp hoặc nó quá bất ổn để sử dụng, cho nên chúng tôi đã phải vô hiệu hóa nó." - }, - "page": { - "title": "Không thể tìm thấy trang", - "description": "Chúng tôi đã tìm kiếm khắp nơi: dưới thùng rác, trong tủ quần áo, đằng sau máy chủ proxy nhưng vẫn không thể tìm thấy trang bạn đang tìm kiếm." - } - }, - "searchBar": { - "movie": "Phim", - "series": "Chương trình truyền hình", - "Search": "Tìm kiếm" - }, - "videoPlayer": { - "findingBestVideo": "Đang tìm nội dung tốt nhất cho bạn", - "noVideos": "Rất tiếc, không tìm thấy nội dung nào cho bạn", - "loading": "Đang tải...", - "backToHome": "Quay lại trang chính", - "backToHomeShort": "Quay lại", - "seasonAndEpisode": "M{{season}} T{{episode}}", - "timeLeft": "Còn {{timeLeft}}", - "finishAt": "Kết thúc vào {{timeFinished, datetime}}", - "buttons": { - "episodes": "Tập", - "source": "Source", - "captions": "Phụ đề", - "download": "Tải xuống", - "settings": "Cài đặt", - "pictureInPicture": "Hình trong hình", - "playbackSpeed": "Tốc độ phát" - }, - "popouts": { - "back": "Quay lại", - "sources": "Nguồn", - "seasons": "Mùa", - "captions": "Phụ đề", - "playbackSpeed": "Tốc độ phát", - "customPlaybackSpeed": "Tủy chỉnh tốc độ phát", - "captionPreferences": { - "title": "Tùy chỉnh", - "delay": "Trì hoãn", - "fontSize": "Kích cỡ", - "opacity": "Độ mờ", - "color": "Màu sắc" - }, - "episode": "T{{index}} - {{title}}", - "noCaptions": "Không phụ đề", - "linkedCaptions": "Phụ đề được liên kết", - "customCaption": "Phụ đề tùy chỉnh", - "uploadCustomCaption": "Tải phụ đề lên", - "noEmbeds": "Không tìm thấy nội dung nhúng nào cho nguồn này", - - "errors": { - "loadingWentWong": "Đã xảy ra lỗi khi tải các tập phim cho {{seasonTitle}}", - "embedsError": "Đã xảy ra lỗi khi tải nội dung nhúng cho nội dung bạn thích này" - }, - "descriptions": { - "sources": "Bạn muốn sử dụng nhà cung cấp nào?", - "embeds": "Chọn video để xem", - "seasons": "Chọn mùa bạn muốn xem", - "episode": "Chọn một tập", - "captions": "Chọn ngôn ngữ của phụ đề", - "captionPreferences": "Làm cho phụ đề trông như thế nào bạn muốn", - "playbackSpeed": "Thay đổi tốc độ phát" - } - }, - "errors": { - "fatalError": "Trình phát video đã gặp phải lỗi nghiêm trọng, vui lòng báo cáo sự cố trên <0>máy chủ Discord hoặc trên <1>GitHub." - } - }, - "settings": { - "title": "Cài đặt", - "language": "Ngôn ngữ", - "captionLanguage": "Ngôn ngữ phụ đề" - }, - "v3": { - "newSiteTitle": "Phiên bản mới đã được phát hành!", - "newDomain": "https://movie-web.app", - "newDomainText": "movie-web sẽ sớm chuyển sang trang mới: <0>https://movie-web.app. Hãy đảm bảo rằng các đánh dấu đã được cập nhật vì <1>trang web cũ sẽ dừng hoạt động vào {{date}}.", - "tireless": "Chúng tôi đã làm việc vất vả để tạo phiên bản mới này, chúng tôi hy vọng bạn sẽ thích những gì chúng tôi đã nung nấu trong những tháng qua.", - "leaveAnnouncement": "Hãy đưa tôi đến đó!" - }, - "casting": { - "casting": "Đang truyền tới thiết bị..." - }, - "errors": { - "offline": "Hãy kiểm tra kết nối Internet của bạn" - } -} From be90b020431d017c4fef23111ed7809a8dceda33 Mon Sep 17 00:00:00 2001 From: spinixster <135334442+spinixster@users.noreply.github.com> Date: Thu, 15 Jun 2023 10:53:08 +0700 Subject: [PATCH 13/45] Update translation.json --- src/setup/locales/vi/translation.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/setup/locales/vi/translation.json b/src/setup/locales/vi/translation.json index 45494388..d56b3595 100644 --- a/src/setup/locales/vi/translation.json +++ b/src/setup/locales/vi/translation.json @@ -20,7 +20,7 @@ "series": "Chương trình truyền hình", "stopEditing": "Hãy dừng chỉnh sửa", "errors": { - "genericTitle": "Rất tiếc, nó đã hỏng!", + "genericTitle": "Rất tiếc, đã hỏng!", "failedMeta": "Không thể tải meta", "mediaFailed": "Chúng tôi không thể tìm thấy nội dung mà bạn yêu cầu, hãy kiểm tra kết nối internet của bạn và thử lại.", "videoFailed": "Chúng tôi đã gặp lỗi khi phát nội dung mà bạn yêu cầu. Nếu điều này tiếp tục xảy ra, vui lòng báo cáo sự cố trên <0>máy chủ Discord hoặc trên <1>GitHub." From 07deb1897d1113460101e6464b61e37d5997ac13 Mon Sep 17 00:00:00 2001 From: spinixster <135334442+spinixster@users.noreply.github.com> Date: Thu, 15 Jun 2023 10:55:02 +0700 Subject: [PATCH 14/45] Update i18n.ts --- src/setup/i18n.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/setup/i18n.ts b/src/setup/i18n.ts index 21388b9f..d0ec7035 100644 --- a/src/setup/i18n.ts +++ b/src/setup/i18n.ts @@ -38,6 +38,9 @@ const locales = { pirate: { translation: pirate, }, + vi: { + translation: vi, + }, }; i18n // pass the i18n instance to react-i18next. From 4bd00eb47a23464611820c6a2cdb87e5bf06ce75 Mon Sep 17 00:00:00 2001 From: Jordaar <69628820+Jordaar@users.noreply.github.com> Date: Fri, 16 Jun 2023 14:37:07 +0530 Subject: [PATCH 15/45] feat(embed): add upcloud and streamsb embed scrapers --- src/backend/embeds/streamsb.ts | 212 +++++++++++++++++++++++++++++++++ src/backend/embeds/upcloud.ts | 93 +++++++++++++++ 2 files changed, 305 insertions(+) create mode 100644 src/backend/embeds/streamsb.ts create mode 100644 src/backend/embeds/upcloud.ts diff --git a/src/backend/embeds/streamsb.ts b/src/backend/embeds/streamsb.ts new file mode 100644 index 00000000..c755a0b0 --- /dev/null +++ b/src/backend/embeds/streamsb.ts @@ -0,0 +1,212 @@ +import Base64 from "crypto-js/enc-base64"; +import Utf8 from "crypto-js/enc-utf8"; + +import { MWEmbedType } from "@/backend/helpers/embed"; +import { proxiedFetch } from "@/backend/helpers/fetch"; +import { registerEmbedScraper } from "@/backend/helpers/register"; +import { + MWCaptionType, + MWStreamQuality, + MWStreamType, +} from "@/backend/helpers/streams"; + +const qualityOrder = [ + MWStreamQuality.Q1080P, + MWStreamQuality.Q720P, + MWStreamQuality.Q480P, + MWStreamQuality.Q360P, +]; + +async function fetchCaptchaToken(domain: string, recaptchaKey: string) { + const domainHash = Base64.stringify(Utf8.parse(domain)).replace(/=/g, "."); + + const recaptchaRender = await proxiedFetch( + `https://www.google.com/recaptcha/api.js?render=${recaptchaKey}` + ); + + const vToken = recaptchaRender.substring( + recaptchaRender.indexOf("/releases/") + 10, + recaptchaRender.indexOf("/recaptcha__en.js") + ); + + const recaptchaAnchor = await proxiedFetch( + `https://www.google.com/recaptcha/api2/anchor?ar=1&hl=en&size=invisible&cb=flicklax&k=${recaptchaKey}&co=${domainHash}&v=${vToken}` + ); + + const cToken = new DOMParser() + .parseFromString(recaptchaAnchor, "text/html") + .getElementById("recaptcha-token") + ?.getAttribute("value"); + + if (!cToken) throw new Error("Unable to find cToken"); + + const payload = { + v: vToken, + reason: "q", + k: recaptchaKey, + c: cToken, + sa: "", + co: domain, + }; + + const tokenData = await proxiedFetch( + `https://www.google.com/recaptcha/api2/reload?${new URLSearchParams( + payload + ).toString()}`, + { + headers: { referer: "https://www.google.com/recaptcha/api2/" }, + method: "POST", + } + ); + + const token = tokenData.match('rresp","(.+?)"'); + return token ? token[1] : null; +} + +registerEmbedScraper({ + id: "streamsb", + displayName: "StreamSB", + for: MWEmbedType.STREAMSB, + rank: 150, + async getStream({ url, progress }) { + /* Url variations + - domain.com/{id}?.html + - domain.com/{id} + - domain.com/embed-{id} + - domain.com/d/{id} + - domain.com/e/{id} + - domain.com/e/{id}-embed + */ + const streamsbUrl = url + .replace(".html", "") + .replace("embed-", "") + .replace("e/", "") + .replace("d/", ""); + + const parsedUrl = new URL(streamsbUrl); + const base = await proxiedFetch( + `${parsedUrl.origin}/d${parsedUrl.pathname}` + ); + + progress(20); + + // Parse captions from url + const captionUrl = parsedUrl.searchParams.get("caption_1"); + const captionLang = parsedUrl.searchParams.get("sub_1"); + + const basePage = new DOMParser().parseFromString(base, "text/html"); + + const downloadVideoFunctions = basePage.querySelectorAll( + "[onclick^=download_video]" + ); + + const dlDetails = []; + for (const func of downloadVideoFunctions) { + const funcContents = func.getAttribute("onclick"); + const regExpFunc = /download_video\('(.+?)','(.+?)','(.+?)'\)/; + const matchesFunc = regExpFunc.exec(funcContents ?? ""); + if (matchesFunc !== null) { + const quality = func.querySelector("span")?.textContent; + const regExpQuality = /(.+?) \((.+?)\)/; + const matchesQuality = regExpQuality.exec(quality ?? ""); + if (matchesQuality !== null) { + dlDetails.push({ + parameters: [matchesFunc[1], matchesFunc[2], matchesFunc[3]], + quality: { + label: matchesQuality[1].trim(), + size: matchesQuality[2], + }, + }); + } + } + } + + progress(40); + + let dls = await Promise.all( + dlDetails.map(async (dl) => { + const getDownload = await proxiedFetch( + `/dl?op=download_orig&id=${dl.parameters[0]}&mode=${dl.parameters[1]}&hash=${dl.parameters[2]}`, + { + baseURL: parsedUrl.origin, + } + ); + + const downloadPage = new DOMParser().parseFromString( + getDownload, + "text/html" + ); + + const recaptchaKey = downloadPage + .querySelector(".g-recaptcha") + ?.getAttribute("data-sitekey"); + if (!recaptchaKey) throw new Error("Unable to get captcha key"); + + const captchaToken = await fetchCaptchaToken( + parsedUrl.origin, + recaptchaKey + ); + if (!captchaToken) throw new Error("Unable to get captcha token"); + + const dlForm = new FormData(); + dlForm.append("op", "download_orig"); + dlForm.append("id", dl.parameters[0]); + dlForm.append("mode", dl.parameters[1]); + dlForm.append("hash", dl.parameters[2]); + dlForm.append("g-recaptcha-response", captchaToken); + + const download = await proxiedFetch( + `/dl?op=download_orig&id=${dl.parameters[0]}&mode=${dl.parameters[1]}&hash=${dl.parameters[2]}`, + { + baseURL: parsedUrl.origin, + method: "POST", + body: dlForm, + } + ); + + const dlLink = new DOMParser() + .parseFromString(download, "text/html") + .querySelector(".btn.btn-light.btn-lg") + ?.getAttribute("href"); + + console.log(dlLink); + + return { + quality: dl.quality.label as MWStreamQuality, + url: dlLink, + size: dl.quality.size, + captions: + captionUrl && captionLang + ? [ + { + url: captionUrl, + langIso: captionLang, + type: MWCaptionType.VTT, + }, + ] + : [], + }; + }) + ); + dls = dls.filter((d) => !!d.url); + dls = dls.sort((a, b) => { + const aQuality = qualityOrder.indexOf(a.quality); + const bQuality = qualityOrder.indexOf(b.quality); + return aQuality - bQuality; + }); + + progress(60); + + // TODO: Quality selection for embed scrapers + const dl = dls[0]; + if (!dl.url) throw new Error("No stream url found"); + + return { + embedId: MWEmbedType.STREAMSB, + streamUrl: dl.url, + quality: dl.quality, + captions: dl.captions, + type: MWStreamType.MP4, + }; + }, +}); diff --git a/src/backend/embeds/upcloud.ts b/src/backend/embeds/upcloud.ts new file mode 100644 index 00000000..b2877bb3 --- /dev/null +++ b/src/backend/embeds/upcloud.ts @@ -0,0 +1,93 @@ +import { AES, enc } from "crypto-js"; + +import { MWEmbedType } from "@/backend/helpers/embed"; +import { registerEmbedScraper } from "@/backend/helpers/register"; +import { + MWCaptionType, + MWStreamQuality, + MWStreamType, +} from "@/backend/helpers/streams"; + +import { proxiedFetch } from "../helpers/fetch"; + +interface StreamRes { + server: number; + sources: string; + tracks: { + file: string; + kind: "captions" | "thumbnails"; + label: string; + }[]; +} + +function isJSON(json: string) { + try { + JSON.parse(json); + return true; + } catch { + return false; + } +} + +registerEmbedScraper({ + id: "upcloud", + displayName: "UpCloud", + for: MWEmbedType.UPCLOUD, + rank: 200, + async getStream({ url }) { + // Example url: https://dokicloud.one/embed-4/{id}?z= + const parsedUrl = new URL(url.replace("embed-5", "embed-4")); + + const dataPath = parsedUrl.pathname.split("/"); + const dataId = dataPath[dataPath.length - 1]; + + const streamRes = await proxiedFetch( + `${parsedUrl.origin}/ajax/embed-4/getSources?id=${dataId}`, + { + headers: { + Referer: parsedUrl.origin, + "X-Requested-With": "XMLHttpRequest", + }, + } + ); + + let sources: + | { + file: string; + type: string; + } + | string = streamRes.sources; + + if (!isJSON(sources) || typeof sources === "string") { + const decryptionKey = await proxiedFetch( + `https://raw.githubusercontent.com/enimax-anime/key/e4/key.txt` + ); + + const decryptedStream = AES.decrypt(sources, decryptionKey).toString( + enc.Utf8 + ); + + const parsedStream = JSON.parse(decryptedStream)[0]; + if (!parsedStream) throw new Error("No stream found"); + sources = parsedStream as { file: string; type: string }; + } + + return { + embedId: MWEmbedType.UPCLOUD, + streamUrl: sources.file, + quality: MWStreamQuality.Q1080P, + type: MWStreamType.HLS, + captions: streamRes.tracks + .filter((sub) => sub.kind === "captions") + .map((sub) => { + return { + langIso: sub.label, + url: sub.file, + type: sub.file.endsWith("vtt") + ? MWCaptionType.VTT + : MWCaptionType.UNKNOWN, + }; + }), + }; + }, +}); From 7e696d5c2cad0e8c4f236cad4d7e1ca47f0f59ea Mon Sep 17 00:00:00 2001 From: Jordaar <69628820+Jordaar@users.noreply.github.com> Date: Fri, 16 Jun 2023 14:37:41 +0530 Subject: [PATCH 16/45] feat(provider): add gomovies provider --- src/backend/providers/gomovies.ts | 162 ++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 src/backend/providers/gomovies.ts diff --git a/src/backend/providers/gomovies.ts b/src/backend/providers/gomovies.ts new file mode 100644 index 00000000..9e22d095 --- /dev/null +++ b/src/backend/providers/gomovies.ts @@ -0,0 +1,162 @@ +import { MWEmbedType } from "../helpers/embed"; +import { proxiedFetch } from "../helpers/fetch"; +import { registerProvider } from "../helpers/register"; +import { MWMediaType } from "../metadata/types"; + +const gomoviesBase = "https://gomovies.sx"; + +registerProvider({ + id: "gomovies", + displayName: "GOmovies", + rank: 300, + type: [MWMediaType.MOVIE, MWMediaType.SERIES], + + async scrape({ media, episode }) { + const search = await proxiedFetch("/ajax/search", { + baseURL: gomoviesBase, + method: "POST", + body: JSON.stringify({ + keyword: media.meta.title, + }), + headers: { + "X-Requested-With": "XMLHttpRequest", + }, + }); + + const searchPage = new DOMParser().parseFromString(search, "text/html"); + const mediaElements = searchPage.querySelectorAll("a.nav-item"); + + const mediaData = Array.from(mediaElements).map((movieEl) => { + const name = movieEl?.querySelector("h3.film-name")?.textContent; + const year = movieEl?.querySelector( + "div.film-infor span:first-of-type" + )?.textContent; + const path = movieEl.getAttribute("href"); + return { name, year, path }; + }); + + const targetMedia = mediaData.find( + (m) => + m.name === media.meta.title && + (media.meta.type === MWMediaType.MOVIE + ? m.year === media.meta.year + : true) + ); + if (!targetMedia?.path) throw new Error("Media not found"); + + // Example movie path: /movie/watch-{slug}-{id} + // Example series path: /tv/watch-{slug}-{id} + let mediaId = targetMedia.path.split("-").pop()?.replace("/", ""); + + let sources = null; + if (media.meta.type === MWMediaType.SERIES) { + const seasons = await proxiedFetch( + `/ajax/v2/tv/seasons/${mediaId}`, + { + baseURL: gomoviesBase, + headers: { + "X-Requested-With": "XMLHttpRequest", + }, + } + ); + + const seasonsEl = new DOMParser() + .parseFromString(seasons, "text/html") + .querySelectorAll(".ss-item"); + + const seasonsData = [...seasonsEl].map((season) => ({ + number: season.innerHTML.replace("Season ", ""), + dataId: season.getAttribute("data-id"), + })); + + const seasonNumber = media.meta.seasonData.number; + const targetSeason = seasonsData.find( + (season) => +season.number === seasonNumber + ); + if (!targetSeason) throw new Error("Season not found"); + + const episodes = await proxiedFetch( + `/ajax/v2/season/episodes/${targetSeason.dataId}`, + { + baseURL: gomoviesBase, + headers: { + "X-Requested-With": "XMLHttpRequest", + }, + } + ); + + const episodesEl = new DOMParser() + .parseFromString(episodes, "text/html") + .querySelectorAll(".eps-item"); + + const episodesData = Array.from(episodesEl).map((ep) => ({ + dataId: ep.getAttribute("data-id"), + number: ep + .querySelector("strong") + ?.textContent?.replace("Eps", "") + .replace(":", "") + .trim(), + })); + + const episodeNumber = media.meta.seasonData.episodes.find( + (e) => e.id === episode + )?.number; + + const targetEpisode = episodesData.find((ep) => + ep.number ? +ep.number : ep.number === episodeNumber + ); + + if (!targetEpisode?.dataId) throw new Error("Episode not found"); + + mediaId = targetEpisode.dataId; + + sources = await proxiedFetch(`/ajax/v2/episode/servers/${mediaId}`, { + baseURL: gomoviesBase, + headers: { + "X-Requested-With": "XMLHttpRequest", + }, + }); + } else { + sources = await proxiedFetch(`/ajax/movie/episodes/${mediaId}`, { + baseURL: gomoviesBase, + headers: { + "X-Requested-With": "XMLHttpRequest", + }, + }); + } + + const upcloud = new DOMParser() + .parseFromString(sources, "text/html") + .querySelector('a[title*="upcloud" i]'); + + const upcloudDataId = + upcloud?.getAttribute("data-id") ?? upcloud?.getAttribute("data-linkid"); + + if (!upcloudDataId) throw new Error("Upcloud source not available"); + + const upcloudSource = await proxiedFetch<{ + type: "iframe" | string; + link: string; + sources: []; + title: string; + tracks: []; + }>(`/ajax/sources/${upcloudDataId}`, { + baseURL: gomoviesBase, + headers: { + "X-Requested-With": "XMLHttpRequest", + }, + }); + + if (!upcloudSource.link || upcloudSource.type !== "iframe") + throw new Error("No upcloud stream found"); + + return { + embeds: [ + { + type: MWEmbedType.UPCLOUD, + url: upcloudSource.link, + }, + ], + }; + }, +}); From d198760f9c0051c4cf91cab4ad2b84762e09c6ae Mon Sep 17 00:00:00 2001 From: Jordaar <69628820+Jordaar@users.noreply.github.com> Date: Fri, 16 Jun 2023 14:37:57 +0530 Subject: [PATCH 17/45] feat(provider): add kissasian provider --- src/backend/providers/kissasian.ts | 103 +++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 src/backend/providers/kissasian.ts diff --git a/src/backend/providers/kissasian.ts b/src/backend/providers/kissasian.ts new file mode 100644 index 00000000..01fa0a2e --- /dev/null +++ b/src/backend/providers/kissasian.ts @@ -0,0 +1,103 @@ +import { MWEmbedType } from "../helpers/embed"; +import { proxiedFetch } from "../helpers/fetch"; +import { registerProvider } from "../helpers/register"; +import { MWMediaType } from "../metadata/types"; + +const kissasianBase = "https://kissasian.li"; + +registerProvider({ + id: "kissasian", + displayName: "KissAsian", + rank: 10000, + type: [MWMediaType.MOVIE, MWMediaType.SERIES], + + async scrape({ media, episode, progress }) { + let seasonNumber = ""; + let episodeNumber = ""; + + if (media.meta.type === MWMediaType.SERIES) { + seasonNumber = + media.meta.seasonData.number === 1 + ? "" + : `${media.meta.seasonData.number}`; + episodeNumber = `${ + media.meta.seasonData.episodes.find((e) => e.id === episode)?.number ?? + "" + }`; + } + + const searchForm = new FormData(); + searchForm.append("keyword", `${media.meta.title} ${seasonNumber}`.trim()); + searchForm.append("type", "Drama"); + + const search = await proxiedFetch("/Search/SearchSuggest", { + baseURL: kissasianBase, + method: "POST", + body: searchForm, + }); + + const searchPage = new DOMParser().parseFromString(search, "text/html"); + + const dramas = Array.from(searchPage.querySelectorAll("a")).map((drama) => { + return { + name: drama.textContent, + url: drama.href, + }; + }); + + const targetDrama = + dramas.find( + (d) => d.name?.toLowerCase() === media.meta.title.toLowerCase() + ) ?? dramas[0]; + if (!targetDrama) throw new Error("Drama not found"); + + progress(30); + + const drama = await proxiedFetch(targetDrama.url); + + const dramaPage = new DOMParser().parseFromString(drama, "text/html"); + + const episodesEl = dramaPage.querySelectorAll("tbody tr:not(:first-child)"); + + const episodes = Array.from(episodesEl) + .map((ep) => { + const number = ep + ?.querySelector("td.episodeSub a") + ?.textContent?.split("Episode")[1] + ?.trim(); + const href = ep?.querySelector("td.episodeSub a")?.getAttribute("href"); + return { number, href }; + }) + .filter((e) => !!e.href); + + const targetEpisode = + media.meta.type === MWMediaType.MOVIE + ? episodes[0] + : episodes.find((e) => e.number === `${episodeNumber}`); + if (!targetEpisode?.href) throw new Error("Episode not found"); + + progress(70); + + const watch = await proxiedFetch(`${targetEpisode.href}&s=sb`, { + baseURL: kissasianBase, + }); + + const watchPage = new DOMParser().parseFromString(watch, "text/html"); + + const streamsbUrl = watchPage + .querySelector("iframe[id=my_video_1]") + ?.getAttribute("src"); + if (!streamsbUrl) throw new Error("Streamsb embed not found"); + + console.log(streamsbUrl); + + return { + embeds: [ + { + type: MWEmbedType.STREAMSB, + url: streamsbUrl, + }, + ], + }; + }, +}); From 2db7e0bef8ffbdf41f74481a21c2c11cc6d6dd2f Mon Sep 17 00:00:00 2001 From: Jordaar <69628820+Jordaar@users.noreply.github.com> Date: Fri, 16 Jun 2023 14:41:30 +0530 Subject: [PATCH 18/45] feat(enum): add upcloud and streamsb enum --- src/backend/helpers/embed.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/backend/helpers/embed.ts b/src/backend/helpers/embed.ts index 0dec6422..a584a0c7 100644 --- a/src/backend/helpers/embed.ts +++ b/src/backend/helpers/embed.ts @@ -4,6 +4,8 @@ export enum MWEmbedType { M4UFREE = "m4ufree", STREAMM4U = "streamm4u", PLAYM4U = "playm4u", + UPCLOUD = "upcloud", + STREAMSB = "streamsb", } export type MWEmbed = { From d4c6dac9f2edecf9c186d8fed18160945e4e7122 Mon Sep 17 00:00:00 2001 From: Jordaar <69628820+Jordaar@users.noreply.github.com> Date: Fri, 16 Jun 2023 14:43:36 +0530 Subject: [PATCH 19/45] disable 2embed --- src/backend/providers/2embed.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/backend/providers/2embed.ts b/src/backend/providers/2embed.ts index 48056020..7cc8938e 100644 --- a/src/backend/providers/2embed.ts +++ b/src/backend/providers/2embed.ts @@ -191,6 +191,7 @@ registerProvider({ displayName: "2Embed", rank: 125, type: [MWMediaType.MOVIE, MWMediaType.SERIES], + disabled: true, // Disabled, not working async scrape({ media, episode, progress }) { let embedUrl = `${twoEmbedBase}/embed/tmdb/movie?id=${media.tmdbId}`; From f6b830d06df314b442ccafdfd6cc9cc303db54ad Mon Sep 17 00:00:00 2001 From: Jordaar <69628820+Jordaar@users.noreply.github.com> Date: Fri, 16 Jun 2023 14:44:54 +0530 Subject: [PATCH 20/45] feat(register): new providers and embed scrapers --- src/backend/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/backend/index.ts b/src/backend/index.ts index eb0ad897..065f1c62 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -9,9 +9,13 @@ import "./providers/m4ufree"; import "./providers/hdwatched"; import "./providers/2embed"; import "./providers/sflix"; +import "./providers/gomovies"; +import "./providers/kissasian"; // embeds import "./embeds/streamm4u"; import "./embeds/playm4u"; +import "./embeds/upcloud"; +import "./embeds/streamsb"; initializeScraperStore(); From 58ca372a49ce1ae8009b1e5f33a4809ef9b4c6a4 Mon Sep 17 00:00:00 2001 From: Jordaar <69628820+Jordaar@users.noreply.github.com> Date: Fri, 16 Jun 2023 14:52:42 +0530 Subject: [PATCH 21/45] refactor(kissasian): change rank --- src/backend/providers/kissasian.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/backend/providers/kissasian.ts b/src/backend/providers/kissasian.ts index 01fa0a2e..8d4e50c4 100644 --- a/src/backend/providers/kissasian.ts +++ b/src/backend/providers/kissasian.ts @@ -8,7 +8,7 @@ const kissasianBase = "https://kissasian.li"; registerProvider({ id: "kissasian", displayName: "KissAsian", - rank: 10000, + rank: 130, type: [MWMediaType.MOVIE, MWMediaType.SERIES], async scrape({ media, episode, progress }) { @@ -89,8 +89,6 @@ registerProvider({ ?.getAttribute("src"); if (!streamsbUrl) throw new Error("Streamsb embed not found"); - console.log(streamsbUrl); - return { embeds: [ { From e912ea4715035b9305a22cbabad3db0f5a4795e3 Mon Sep 17 00:00:00 2001 From: Jordaar <69628820+Jordaar@users.noreply.github.com> Date: Fri, 16 Jun 2023 15:05:42 +0530 Subject: [PATCH 22/45] cleanup --- src/backend/embeds/streamsb.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/backend/embeds/streamsb.ts b/src/backend/embeds/streamsb.ts index c755a0b0..99ed563e 100644 --- a/src/backend/embeds/streamsb.ts +++ b/src/backend/embeds/streamsb.ts @@ -169,8 +169,6 @@ registerEmbedScraper({ .querySelector(".btn.btn-light.btn-lg") ?.getAttribute("href"); - console.log(dlLink); - return { quality: dl.quality.label as MWStreamQuality, url: dlLink, From 9003bf67887e4750e47a093db1a2b7a3ca8e4703 Mon Sep 17 00:00:00 2001 From: Jordaar <69628820+Jordaar@users.noreply.github.com> Date: Fri, 16 Jun 2023 16:12:07 +0530 Subject: [PATCH 23/45] feat(embed): add mp4upload embed scraper --- src/backend/embeds/mp4upload.ts | 32 ++++++++++++++++++++++++++++++++ src/backend/index.ts | 1 + 2 files changed, 33 insertions(+) create mode 100644 src/backend/embeds/mp4upload.ts diff --git a/src/backend/embeds/mp4upload.ts b/src/backend/embeds/mp4upload.ts new file mode 100644 index 00000000..3902e20b --- /dev/null +++ b/src/backend/embeds/mp4upload.ts @@ -0,0 +1,32 @@ +import { MWEmbedType } from "@/backend/helpers/embed"; +import { registerEmbedScraper } from "@/backend/helpers/register"; +import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams"; + +import { proxiedFetch } from "../helpers/fetch"; + +registerEmbedScraper({ + id: "mp4upload", + displayName: "mp4upload", + for: MWEmbedType.MP4UPLOAD, + rank: 170, + async getStream({ url }) { + const embed = await proxiedFetch(url); + + const playerSrcRegex = + /(?<=player\.src\()\s*{\s*type:\s*"[^"]+",\s*src:\s*"([^"]+)"\s*}\s*(?=\);)/s; + + const playerSrc = embed.match(playerSrcRegex); + + const streamUrl = playerSrc[1]; + + if (!streamUrl) throw new Error("Stream url not found"); + + return { + embedId: MWEmbedType.MP4UPLOAD, + streamUrl, + quality: MWStreamQuality.Q1080P, + captions: [], + type: MWStreamType.MP4, + }; + }, +}); diff --git a/src/backend/index.ts b/src/backend/index.ts index 065f1c62..a2beaa2a 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -17,5 +17,6 @@ import "./embeds/streamm4u"; import "./embeds/playm4u"; import "./embeds/upcloud"; import "./embeds/streamsb"; +import "./embeds/mp4upload"; initializeScraperStore(); From 7e948c60c1473dd4bda958add89c62dacdef74a3 Mon Sep 17 00:00:00 2001 From: Jordaar <69628820+Jordaar@users.noreply.github.com> Date: Fri, 16 Jun 2023 16:12:53 +0530 Subject: [PATCH 24/45] feat(enum): add mp4upload enum --- src/backend/helpers/embed.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/backend/helpers/embed.ts b/src/backend/helpers/embed.ts index a584a0c7..1ec3362c 100644 --- a/src/backend/helpers/embed.ts +++ b/src/backend/helpers/embed.ts @@ -6,6 +6,7 @@ export enum MWEmbedType { PLAYM4U = "playm4u", UPCLOUD = "upcloud", STREAMSB = "streamsb", + MP4UPLOAD = "mp4upload", } export type MWEmbed = { From a0bb03790a37fad2eed1ab15f4ada9814a84dc45 Mon Sep 17 00:00:00 2001 From: Jordaar <69628820+Jordaar@users.noreply.github.com> Date: Fri, 16 Jun 2023 16:14:05 +0530 Subject: [PATCH 25/45] refactor(streamsb): improve quality sorting --- src/backend/embeds/streamsb.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/backend/embeds/streamsb.ts b/src/backend/embeds/streamsb.ts index 99ed563e..e91b43c7 100644 --- a/src/backend/embeds/streamsb.ts +++ b/src/backend/embeds/streamsb.ts @@ -100,7 +100,7 @@ registerEmbedScraper({ "[onclick^=download_video]" ); - const dlDetails = []; + let dlDetails = []; for (const func of downloadVideoFunctions) { const funcContents = func.getAttribute("onclick"); const regExpFunc = /download_video\('(.+?)','(.+?)','(.+?)'\)/; @@ -121,6 +121,12 @@ registerEmbedScraper({ } } + dlDetails = dlDetails.sort((a, b) => { + const aQuality = qualityOrder.indexOf(a.quality.label as MWStreamQuality); + const bQuality = qualityOrder.indexOf(b.quality.label as MWStreamQuality); + return aQuality - bQuality; + }); + progress(40); let dls = await Promise.all( @@ -187,11 +193,6 @@ registerEmbedScraper({ }) ); dls = dls.filter((d) => !!d.url); - dls = dls.sort((a, b) => { - const aQuality = qualityOrder.indexOf(a.quality); - const bQuality = qualityOrder.indexOf(b.quality); - return aQuality - bQuality; - }); progress(60); From bc0f9a6abff9ab90b7106456f852de9fffbf8276 Mon Sep 17 00:00:00 2001 From: Jordaar <69628820+Jordaar@users.noreply.github.com> Date: Fri, 16 Jun 2023 16:15:41 +0530 Subject: [PATCH 26/45] feat(kissasian): additional mp4upload embed scraper --- src/backend/providers/kissasian.ts | 54 ++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/src/backend/providers/kissasian.ts b/src/backend/providers/kissasian.ts index 8d4e50c4..90708970 100644 --- a/src/backend/providers/kissasian.ts +++ b/src/backend/providers/kissasian.ts @@ -5,6 +5,17 @@ import { MWMediaType } from "../metadata/types"; const kissasianBase = "https://kissasian.li"; +const embedProviders = [ + { + type: MWEmbedType.MP4UPLOAD, + id: "mp", + }, + { + type: MWEmbedType.STREAMSB, + id: "sb", + }, +]; + registerProvider({ id: "kissasian", displayName: "KissAsian", @@ -65,37 +76,44 @@ registerProvider({ ?.querySelector("td.episodeSub a") ?.textContent?.split("Episode")[1] ?.trim(); - const href = ep?.querySelector("td.episodeSub a")?.getAttribute("href"); - return { number, href }; + const url = ep?.querySelector("td.episodeSub a")?.getAttribute("href"); + return { number, url }; }) - .filter((e) => !!e.href); + .filter((e) => !!e.url); const targetEpisode = media.meta.type === MWMediaType.MOVIE ? episodes[0] : episodes.find((e) => e.number === `${episodeNumber}`); - if (!targetEpisode?.href) throw new Error("Episode not found"); + if (!targetEpisode?.url) throw new Error("Episode not found"); progress(70); - const watch = await proxiedFetch(`${targetEpisode.href}&s=sb`, { - baseURL: kissasianBase, - }); + let embeds = await Promise.all( + embedProviders.map(async (provider) => { + const watch = await proxiedFetch( + `${targetEpisode.url}&s=${provider.id}`, + { + baseURL: kissasianBase, + } + ); - const watchPage = new DOMParser().parseFromString(watch, "text/html"); + const watchPage = new DOMParser().parseFromString(watch, "text/html"); - const streamsbUrl = watchPage - .querySelector("iframe[id=my_video_1]") - ?.getAttribute("src"); - if (!streamsbUrl) throw new Error("Streamsb embed not found"); + const embedUrl = watchPage + .querySelector("iframe[id=my_video_1]") + ?.getAttribute("src"); + + return { + type: provider.type, + url: embedUrl ?? "", + }; + }) + ); + embeds = embeds.filter((e) => e.url !== ""); return { - embeds: [ - { - type: MWEmbedType.STREAMSB, - url: streamsbUrl, - }, - ], + embeds, }; }, }); From d47acada58f660cf4bc0788fd6b6a80d5e6c2729 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sat, 17 Jun 2023 20:20:38 +0200 Subject: [PATCH 27/45] Update i18n.ts --- src/setup/i18n.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/setup/i18n.ts b/src/setup/i18n.ts index 919754ae..ccb77d22 100644 --- a/src/setup/i18n.ts +++ b/src/setup/i18n.ts @@ -45,7 +45,7 @@ const locales = { }, vi: { translation: vi, - } + }, pl: { translation: pl, }, From a4bd9bb87a09da7e1880089816e578053746de30 Mon Sep 17 00:00:00 2001 From: frost768 Date: Sun, 18 Jun 2023 15:10:26 +0300 Subject: [PATCH 28/45] fix: language preference persistence --- src/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/index.tsx b/src/index.tsx index 1bf99f70..36b1fb14 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -8,13 +8,14 @@ import { registerSW } from "virtual:pwa-register"; import { ErrorBoundary } from "@/components/layout/ErrorBoundary"; import App from "@/setup/App"; import { conf } from "@/setup/config"; +import i18n from "@/setup/i18n"; import "@/setup/ga"; import "@/setup/sentry"; -import "@/setup/i18n"; import "@/setup/index.css"; import "@/backend"; import { initializeChromecast } from "./setup/chromecast"; +import { SettingsStore } from "./state/settings/store"; import { initializeStores } from "./utils/storage"; // initialize @@ -30,6 +31,7 @@ registerSW({ const LazyLoadedApp = React.lazy(async () => { await initializeStores(); + i18n.changeLanguage(SettingsStore.get().language ?? "en"); return { default: App, }; From 70852773f94c30ab47d9737bff8a9875f514c564 Mon Sep 17 00:00:00 2001 From: castdrian Date: Mon, 12 Jun 2023 20:06:46 +0200 Subject: [PATCH 29/45] partial refactor --- src/backend/metadata/search_new.ts | 21 +++ src/backend/metadata/tmdb.ts | 78 +++++++++ src/backend/metadata/trakttv.ts | 166 ++++++++++++++++++ src/backend/metadata/types_new.ts | 264 +++++++++++++++++++++++++++++ 4 files changed, 529 insertions(+) create mode 100644 src/backend/metadata/search_new.ts create mode 100644 src/backend/metadata/tmdb.ts create mode 100644 src/backend/metadata/trakttv.ts create mode 100644 src/backend/metadata/types_new.ts diff --git a/src/backend/metadata/search_new.ts b/src/backend/metadata/search_new.ts new file mode 100644 index 00000000..4506514a --- /dev/null +++ b/src/backend/metadata/search_new.ts @@ -0,0 +1,21 @@ +import { SimpleCache } from "@/utils/cache"; + +import { Trakt, mediaTypeToTTV } from "./trakttv"; +import { MWMediaMeta, MWQuery } from "./types"; + +const cache = new SimpleCache(); +cache.setCompare((a, b) => { + return a.type === b.type && a.searchQuery.trim() === b.searchQuery.trim(); +}); +cache.initialize(); + +export async function searchForMedia(query: MWQuery): Promise { + if (cache.has(query)) return cache.get(query) as MWMediaMeta[]; + const { searchQuery, type } = query; + + const contentType = mediaTypeToTTV(type); + + const results = await Trakt.search(searchQuery, contentType); + cache.set(query, results, 3600); + return results; +} diff --git a/src/backend/metadata/tmdb.ts b/src/backend/metadata/tmdb.ts new file mode 100644 index 00000000..0700945b --- /dev/null +++ b/src/backend/metadata/tmdb.ts @@ -0,0 +1,78 @@ +import { conf } from "@/setup/config"; + +import { + DetailedMeta, + MWMediaType, + TMDBMediaStatic, + TMDBMovieData, + TMDBShowData, +} from "./types"; +import { mwFetch } from "../helpers/fetch"; + +export abstract class Tmdb { + private static baseURL = "https://api.themoviedb.org/3"; + + private static headers = { + accept: "application/json", + Authorization: `Bearer ${conf().TMDB_API_KEY}`, + }; + + private static async get(url: string): Promise { + const res = await mwFetch(url, { + headers: Tmdb.headers, + baseURL: Tmdb.baseURL, + }); + return res; + } + + public static getMediaDetails: TMDBMediaStatic["getMediaDetails"] = async ( + id: string, + type: MWMediaType + ) => { + let data; + + switch (type) { + case "movie": + data = await Tmdb.get(`/movie/${id}`); + break; + case "series": + data = await Tmdb.get(`/tv/${id}`); + break; + default: + throw new Error("Invalid media type"); + } + + return data; + }; + + public static getMediaPoster(posterPath: string | null): string | undefined { + if (posterPath) return `https://image.tmdb.org/t/p/w185/${posterPath}`; + } + + /* public static async getMetaFromId( + type: MWMediaType, + id: string, + seasonId?: string + ): Promise { + console.log("getMetaFromId", type, id, seasonId); + + const details = await Tmdb.getMediaDetails(id, type); + + if (!details) return null; + + let imdbId; + if (type === MWMediaType.MOVIE) { + imdbId = (details as TMDBMovieData).imdb_id ?? undefined; + } + + if (!meta.length) return null; + + console.log(meta); + + return { + meta, + imdbId, + tmdbId: id, + }; + } */ +} diff --git a/src/backend/metadata/trakttv.ts b/src/backend/metadata/trakttv.ts new file mode 100644 index 00000000..5fb67a17 --- /dev/null +++ b/src/backend/metadata/trakttv.ts @@ -0,0 +1,166 @@ +import { conf } from "@/setup/config"; + +import { Tmdb } from "./tmdb"; +import { + DetailedMeta, + MWMediaMeta, + MWMediaType, + MWSeasonMeta, + TMDBShowData, + TTVContentTypes, + TTVMediaResult, + TTVSearchResult, + TTVSeasonMetaResult, +} from "./types"; +import { mwFetch } from "../helpers/fetch"; + +export function mediaTypeToTTV(type: MWMediaType): TTVContentTypes { + if (type === MWMediaType.MOVIE) return "movie"; + if (type === MWMediaType.SERIES) return "show"; + throw new Error("unsupported type"); +} + +export function TTVMediaToMediaType(type: string): MWMediaType { + if (type === "movie") return MWMediaType.MOVIE; + if (type === "show") return MWMediaType.SERIES; + throw new Error("unsupported type"); +} + +export function formatTTVMeta( + media: TTVMediaResult, + season?: TTVSeasonMetaResult +): MWMediaMeta { + const type = TTVMediaToMediaType(media.object_type); + let seasons: undefined | MWSeasonMeta[]; + if (type === MWMediaType.SERIES) { + seasons = media.seasons + ?.sort((a, b) => a.season_number - b.season_number) + .map( + (v): MWSeasonMeta => ({ + title: v.title, + id: v.id.toString(), + number: v.season_number, + }) + ); + } + + return { + title: media.title, + id: media.id.toString(), + year: media.original_release_year?.toString(), + poster: media.poster, + type, + seasons: seasons as any, + seasonData: season + ? ({ + id: season.id.toString(), + number: season.season_number, + title: season.title, + episodes: season.episodes + .sort((a, b) => a.episode_number - b.episode_number) + .map((v) => ({ + id: v.id.toString(), + number: v.episode_number, + title: v.title, + })), + } as any) + : (undefined as any), + }; +} + +export function TTVMediaToId(media: MWMediaMeta): string { + return ["TTV", mediaTypeToTTV(media.type), media.id].join("-"); +} + +export function decodeTTVId( + paramId: string +): { id: string; type: MWMediaType } | null { + const [prefix, type, id] = paramId.split("-", 3); + if (prefix !== "TTV") return null; + let mediaType; + try { + mediaType = TTVMediaToMediaType(type); + } catch { + return null; + } + return { + type: mediaType, + id, + }; +} + +export async function formatTTVSearchResult( + result: TTVSearchResult +): Promise { + const type = TTVMediaToMediaType(result.type); + const media = result[result.type]; + + if (!media) throw new Error("invalid result"); + + const details = await Tmdb.getMediaDetails( + media.ids.tmdb.toString(), + TTVMediaToMediaType(result.type) + ); + console.log(details); + + const seasons = + type === MWMediaType.SERIES + ? (details as TMDBShowData).seasons?.map((v) => ({ + id: v.id, + title: v.name, + season_number: v.season_number, + })) + : undefined; + + return { + title: media.title, + poster: Tmdb.getMediaPoster(details.poster_path), + id: media.ids.trakt, + original_release_year: media.year, + ttv_entity_id: media.ids.slug, + object_type: mediaTypeToTTV(type), + seasons, + }; +} + +export abstract class Trakt { + private static baseURL = "https://api.trakt.tv"; + + private static headers = { + "Content-Type": "application/json", + "trakt-api-version": "2", + "trakt-api-key": conf().TRAKT_CLIENT_ID, + }; + + private static async get(url: string): Promise { + const res = await mwFetch(url, { + headers: Trakt.headers, + baseURL: Trakt.baseURL, + }); + return res; + } + + public static async search( + query: string, + type: "movie" | "show" + ): Promise { + const data = await Trakt.get( + `/search/${type}?query=${encodeURIComponent(query)}` + ); + + const formatted = await Promise.all( + // eslint-disable-next-line no-return-await + data.map(async (v) => await formatTTVSearchResult(v)) + ); + return formatted.map((v) => formatTTVMeta(v)); + } + + public static async getMetaFromId( + type: MWMediaType, + id: string, + seasonId?: string + ): Promise { + console.log("getMetaFromId", type, id, seasonId); + return null; + } +} diff --git a/src/backend/metadata/types_new.ts b/src/backend/metadata/types_new.ts new file mode 100644 index 00000000..f954ff6a --- /dev/null +++ b/src/backend/metadata/types_new.ts @@ -0,0 +1,264 @@ +export enum MWMediaType { + MOVIE = "movie", + SERIES = "series", + ANIME = "anime", +} + +export type MWSeasonMeta = { + id: string; + number: number; + title: string; +}; + +export type MWSeasonWithEpisodeMeta = { + id: string; + number: number; + title: string; + episodes: { + id: string; + number: number; + title: string; + }[]; +}; + +type MWMediaMetaBase = { + title: string; + id: string; + year?: string; + poster?: string; +}; + +type MWMediaMetaSpecific = + | { + type: MWMediaType.MOVIE | MWMediaType.ANIME; + seasons: undefined; + } + | { + type: MWMediaType.SERIES; + seasons: MWSeasonMeta[]; + seasonData: MWSeasonWithEpisodeMeta; + }; + +export type MWMediaMeta = MWMediaMetaBase & MWMediaMetaSpecific; + +export interface MWQuery { + searchQuery: string; + type: MWMediaType; +} + +export type TTVContentTypes = "movie" | "show"; + +export type TTVSeasonShort = { + title: string; + id: number; + season_number: number; +}; + +export type TTVEpisodeShort = { + title: string; + id: number; + episode_number: number; +}; + +export type TTVMediaResult = { + title: string; + poster?: string; + id: number; + original_release_year?: number; + ttv_entity_id: string; + object_type: TTVContentTypes; + seasons?: TTVSeasonShort[]; +}; + +export type TTVSeasonMetaResult = { + title: string; + id: string; + season_number: number; + episodes: TTVEpisodeShort[]; +}; + +export interface TTVSearchResult { + type: "movie" | "show"; + score: number; + movie?: { + title: string; + year: number; + ids: { + trakt: number; + slug: string; + imdb: string; + tmdb: number; + }; + }; + show?: { + title: string; + year: number; + ids: { + trakt: number; + slug: string; + tvdb: number; + imdb: string; + tmdb: number; + }; + }; +} + +export interface DetailedMeta { + meta: MWMediaMeta; + imdbId?: string; + tmdbId?: string; +} + +export interface TMDBShowData { + adult: boolean; + backdrop_path: string | null; + created_by: { + id: number; + credit_id: string; + name: string; + gender: number; + profile_path: string | null; + }[]; + episode_run_time: number[]; + first_air_date: string; + genres: { + id: number; + name: string; + }[]; + homepage: string; + id: number; + in_production: boolean; + languages: string[]; + last_air_date: string; + last_episode_to_air: { + id: number; + name: string; + overview: string; + vote_average: number; + vote_count: number; + air_date: string; + episode_number: number; + production_code: string; + runtime: number | null; + season_number: number; + show_id: number; + still_path: string | null; + } | null; + name: string; + next_episode_to_air: { + id: number; + name: string; + overview: string; + vote_average: number; + vote_count: number; + air_date: string; + episode_number: number; + production_code: string; + runtime: number | null; + season_number: number; + show_id: number; + still_path: string | null; + } | null; + networks: { + id: number; + logo_path: string; + name: string; + origin_country: string; + }[]; + number_of_episodes: number; + number_of_seasons: number; + origin_country: string[]; + original_language: string; + original_name: string; + overview: string; + popularity: number; + poster_path: string | null; + production_companies: { + id: number; + logo_path: string | null; + name: string; + origin_country: string; + }[]; + production_countries: { + iso_3166_1: string; + name: string; + }[]; + seasons: { + air_date: string; + episode_count: number; + id: number; + name: string; + overview: string; + poster_path: string | null; + season_number: number; + }[]; + spoken_languages: { + english_name: string; + iso_639_1: string; + name: string; + }[]; + status: string; + tagline: string; + type: string; + vote_average: number; + vote_count: number; +} + +export interface TMDBMovieData { + adult: boolean; + backdrop_path: string | null; + belongs_to_collection: { + id: number; + name: string; + poster_path: string | null; + backdrop_path: string | null; + } | null; + budget: number; + genres: { + id: number; + name: string; + }[]; + homepage: string | null; + id: number; + imdb_id: string | null; + original_language: string; + original_title: string; + overview: string | null; + popularity: number; + poster_path: string | null; + production_companies: { + id: number; + logo_path: string | null; + name: string; + origin_country: string; + }[]; + production_countries: { + iso_3166_1: string; + name: string; + }[]; + release_date: string; + revenue: number; + runtime: number | null; + spoken_languages: { + english_name: string; + iso_639_1: string; + name: string; + }[]; + status: string; + tagline: string | null; + title: string; + video: boolean; + vote_average: number; + vote_count: number; +} + +export type TMDBMediaDetailsPromise = Promise; + +export interface TMDBMediaStatic { + getMediaDetails( + id: string, + type: MWMediaType.SERIES + ): TMDBMediaDetailsPromise; + getMediaDetails(id: string, type: MWMediaType.MOVIE): TMDBMediaDetailsPromise; + getMediaDetails(id: string, type: MWMediaType): TMDBMediaDetailsPromise; +} From 63f26b81dec2bab16f6d19c0d502a05753d85497 Mon Sep 17 00:00:00 2001 From: castdrian Date: Mon, 12 Jun 2023 20:17:42 +0200 Subject: [PATCH 30/45] preliminary refactor --- src/backend/metadata/search.ts | 48 +----- src/backend/metadata/search_new.ts | 21 --- src/backend/metadata/search_old.ts | 59 +++++++ src/backend/metadata/tmdb.ts | 1 - src/backend/metadata/types.ts | 217 ++++++++++++++++++++++++ src/backend/metadata/types_new.ts | 264 ----------------------------- src/backend/metadata/types_old.ts | 47 +++++ src/setup/config.ts | 4 + 8 files changed, 332 insertions(+), 329 deletions(-) delete mode 100644 src/backend/metadata/search_new.ts create mode 100644 src/backend/metadata/search_old.ts delete mode 100644 src/backend/metadata/types_new.ts create mode 100644 src/backend/metadata/types_old.ts diff --git a/src/backend/metadata/search.ts b/src/backend/metadata/search.ts index 10cbb285..4506514a 100644 --- a/src/backend/metadata/search.ts +++ b/src/backend/metadata/search.ts @@ -1,14 +1,7 @@ import { SimpleCache } from "@/utils/cache"; -import { - JWContentTypes, - JWMediaResult, - JW_API_BASE, - formatJWMeta, - mediaTypeToJW, -} from "./justwatch"; +import { Trakt, mediaTypeToTTV } from "./trakttv"; import { MWMediaMeta, MWQuery } from "./types"; -import { proxiedFetch } from "../helpers/fetch"; const cache = new SimpleCache(); cache.setCompare((a, b) => { @@ -16,44 +9,13 @@ cache.setCompare((a, b) => { }); cache.initialize(); -type JWSearchQuery = { - content_types: JWContentTypes[]; - page: number; - page_size: number; - query: string; -}; - -type JWPage = { - items: T[]; - page: number; - page_size: number; - total_pages: number; - total_results: number; -}; - export async function searchForMedia(query: MWQuery): Promise { if (cache.has(query)) return cache.get(query) as MWMediaMeta[]; const { searchQuery, type } = query; - const contentType = mediaTypeToJW(type); - const body: JWSearchQuery = { - content_types: [contentType], - page: 1, - query: searchQuery, - page_size: 40, - }; + const contentType = mediaTypeToTTV(type); - const data = await proxiedFetch>( - "/content/titles/en_US/popular", - { - baseURL: JW_API_BASE, - params: { - body: JSON.stringify(body), - }, - } - ); - - const returnData = data.items.map((v) => formatJWMeta(v)); - cache.set(query, returnData, 3600); // cache for an hour - return returnData; + const results = await Trakt.search(searchQuery, contentType); + cache.set(query, results, 3600); + return results; } diff --git a/src/backend/metadata/search_new.ts b/src/backend/metadata/search_new.ts deleted file mode 100644 index 4506514a..00000000 --- a/src/backend/metadata/search_new.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { SimpleCache } from "@/utils/cache"; - -import { Trakt, mediaTypeToTTV } from "./trakttv"; -import { MWMediaMeta, MWQuery } from "./types"; - -const cache = new SimpleCache(); -cache.setCompare((a, b) => { - return a.type === b.type && a.searchQuery.trim() === b.searchQuery.trim(); -}); -cache.initialize(); - -export async function searchForMedia(query: MWQuery): Promise { - if (cache.has(query)) return cache.get(query) as MWMediaMeta[]; - const { searchQuery, type } = query; - - const contentType = mediaTypeToTTV(type); - - const results = await Trakt.search(searchQuery, contentType); - cache.set(query, results, 3600); - return results; -} diff --git a/src/backend/metadata/search_old.ts b/src/backend/metadata/search_old.ts new file mode 100644 index 00000000..05be994d --- /dev/null +++ b/src/backend/metadata/search_old.ts @@ -0,0 +1,59 @@ +import { SimpleCache } from "@/utils/cache"; + +import { + JWContentTypes, + JWMediaResult, + JW_API_BASE, + formatJWMeta, + mediaTypeToJW, +} from "./justwatch"; +import { MWMediaMeta, MWQuery } from "./types_old"; +import { proxiedFetch } from "../helpers/fetch"; + +const cache = new SimpleCache(); +cache.setCompare((a, b) => { + return a.type === b.type && a.searchQuery.trim() === b.searchQuery.trim(); +}); +cache.initialize(); + +type JWSearchQuery = { + content_types: JWContentTypes[]; + page: number; + page_size: number; + query: string; +}; + +type JWPage = { + items: T[]; + page: number; + page_size: number; + total_pages: number; + total_results: number; +}; + +export async function searchForMedia(query: MWQuery): Promise { + if (cache.has(query)) return cache.get(query) as MWMediaMeta[]; + const { searchQuery, type } = query; + + const contentType = mediaTypeToJW(type); + const body: JWSearchQuery = { + content_types: [contentType], + page: 1, + query: searchQuery, + page_size: 40, + }; + + const data = await proxiedFetch>( + "/content/titles/en_US/popular", + { + baseURL: JW_API_BASE, + params: { + body: JSON.stringify(body), + }, + } + ); + + const returnData = data.items.map((v) => formatJWMeta(v)); + cache.set(query, returnData, 3600); // cache for an hour + return returnData; +} diff --git a/src/backend/metadata/tmdb.ts b/src/backend/metadata/tmdb.ts index 0700945b..460e13b4 100644 --- a/src/backend/metadata/tmdb.ts +++ b/src/backend/metadata/tmdb.ts @@ -1,7 +1,6 @@ import { conf } from "@/setup/config"; import { - DetailedMeta, MWMediaType, TMDBMediaStatic, TMDBMovieData, diff --git a/src/backend/metadata/types.ts b/src/backend/metadata/types.ts index 2723fbe7..f954ff6a 100644 --- a/src/backend/metadata/types.ts +++ b/src/backend/metadata/types.ts @@ -45,3 +45,220 @@ export interface MWQuery { searchQuery: string; type: MWMediaType; } + +export type TTVContentTypes = "movie" | "show"; + +export type TTVSeasonShort = { + title: string; + id: number; + season_number: number; +}; + +export type TTVEpisodeShort = { + title: string; + id: number; + episode_number: number; +}; + +export type TTVMediaResult = { + title: string; + poster?: string; + id: number; + original_release_year?: number; + ttv_entity_id: string; + object_type: TTVContentTypes; + seasons?: TTVSeasonShort[]; +}; + +export type TTVSeasonMetaResult = { + title: string; + id: string; + season_number: number; + episodes: TTVEpisodeShort[]; +}; + +export interface TTVSearchResult { + type: "movie" | "show"; + score: number; + movie?: { + title: string; + year: number; + ids: { + trakt: number; + slug: string; + imdb: string; + tmdb: number; + }; + }; + show?: { + title: string; + year: number; + ids: { + trakt: number; + slug: string; + tvdb: number; + imdb: string; + tmdb: number; + }; + }; +} + +export interface DetailedMeta { + meta: MWMediaMeta; + imdbId?: string; + tmdbId?: string; +} + +export interface TMDBShowData { + adult: boolean; + backdrop_path: string | null; + created_by: { + id: number; + credit_id: string; + name: string; + gender: number; + profile_path: string | null; + }[]; + episode_run_time: number[]; + first_air_date: string; + genres: { + id: number; + name: string; + }[]; + homepage: string; + id: number; + in_production: boolean; + languages: string[]; + last_air_date: string; + last_episode_to_air: { + id: number; + name: string; + overview: string; + vote_average: number; + vote_count: number; + air_date: string; + episode_number: number; + production_code: string; + runtime: number | null; + season_number: number; + show_id: number; + still_path: string | null; + } | null; + name: string; + next_episode_to_air: { + id: number; + name: string; + overview: string; + vote_average: number; + vote_count: number; + air_date: string; + episode_number: number; + production_code: string; + runtime: number | null; + season_number: number; + show_id: number; + still_path: string | null; + } | null; + networks: { + id: number; + logo_path: string; + name: string; + origin_country: string; + }[]; + number_of_episodes: number; + number_of_seasons: number; + origin_country: string[]; + original_language: string; + original_name: string; + overview: string; + popularity: number; + poster_path: string | null; + production_companies: { + id: number; + logo_path: string | null; + name: string; + origin_country: string; + }[]; + production_countries: { + iso_3166_1: string; + name: string; + }[]; + seasons: { + air_date: string; + episode_count: number; + id: number; + name: string; + overview: string; + poster_path: string | null; + season_number: number; + }[]; + spoken_languages: { + english_name: string; + iso_639_1: string; + name: string; + }[]; + status: string; + tagline: string; + type: string; + vote_average: number; + vote_count: number; +} + +export interface TMDBMovieData { + adult: boolean; + backdrop_path: string | null; + belongs_to_collection: { + id: number; + name: string; + poster_path: string | null; + backdrop_path: string | null; + } | null; + budget: number; + genres: { + id: number; + name: string; + }[]; + homepage: string | null; + id: number; + imdb_id: string | null; + original_language: string; + original_title: string; + overview: string | null; + popularity: number; + poster_path: string | null; + production_companies: { + id: number; + logo_path: string | null; + name: string; + origin_country: string; + }[]; + production_countries: { + iso_3166_1: string; + name: string; + }[]; + release_date: string; + revenue: number; + runtime: number | null; + spoken_languages: { + english_name: string; + iso_639_1: string; + name: string; + }[]; + status: string; + tagline: string | null; + title: string; + video: boolean; + vote_average: number; + vote_count: number; +} + +export type TMDBMediaDetailsPromise = Promise; + +export interface TMDBMediaStatic { + getMediaDetails( + id: string, + type: MWMediaType.SERIES + ): TMDBMediaDetailsPromise; + getMediaDetails(id: string, type: MWMediaType.MOVIE): TMDBMediaDetailsPromise; + getMediaDetails(id: string, type: MWMediaType): TMDBMediaDetailsPromise; +} diff --git a/src/backend/metadata/types_new.ts b/src/backend/metadata/types_new.ts deleted file mode 100644 index f954ff6a..00000000 --- a/src/backend/metadata/types_new.ts +++ /dev/null @@ -1,264 +0,0 @@ -export enum MWMediaType { - MOVIE = "movie", - SERIES = "series", - ANIME = "anime", -} - -export type MWSeasonMeta = { - id: string; - number: number; - title: string; -}; - -export type MWSeasonWithEpisodeMeta = { - id: string; - number: number; - title: string; - episodes: { - id: string; - number: number; - title: string; - }[]; -}; - -type MWMediaMetaBase = { - title: string; - id: string; - year?: string; - poster?: string; -}; - -type MWMediaMetaSpecific = - | { - type: MWMediaType.MOVIE | MWMediaType.ANIME; - seasons: undefined; - } - | { - type: MWMediaType.SERIES; - seasons: MWSeasonMeta[]; - seasonData: MWSeasonWithEpisodeMeta; - }; - -export type MWMediaMeta = MWMediaMetaBase & MWMediaMetaSpecific; - -export interface MWQuery { - searchQuery: string; - type: MWMediaType; -} - -export type TTVContentTypes = "movie" | "show"; - -export type TTVSeasonShort = { - title: string; - id: number; - season_number: number; -}; - -export type TTVEpisodeShort = { - title: string; - id: number; - episode_number: number; -}; - -export type TTVMediaResult = { - title: string; - poster?: string; - id: number; - original_release_year?: number; - ttv_entity_id: string; - object_type: TTVContentTypes; - seasons?: TTVSeasonShort[]; -}; - -export type TTVSeasonMetaResult = { - title: string; - id: string; - season_number: number; - episodes: TTVEpisodeShort[]; -}; - -export interface TTVSearchResult { - type: "movie" | "show"; - score: number; - movie?: { - title: string; - year: number; - ids: { - trakt: number; - slug: string; - imdb: string; - tmdb: number; - }; - }; - show?: { - title: string; - year: number; - ids: { - trakt: number; - slug: string; - tvdb: number; - imdb: string; - tmdb: number; - }; - }; -} - -export interface DetailedMeta { - meta: MWMediaMeta; - imdbId?: string; - tmdbId?: string; -} - -export interface TMDBShowData { - adult: boolean; - backdrop_path: string | null; - created_by: { - id: number; - credit_id: string; - name: string; - gender: number; - profile_path: string | null; - }[]; - episode_run_time: number[]; - first_air_date: string; - genres: { - id: number; - name: string; - }[]; - homepage: string; - id: number; - in_production: boolean; - languages: string[]; - last_air_date: string; - last_episode_to_air: { - id: number; - name: string; - overview: string; - vote_average: number; - vote_count: number; - air_date: string; - episode_number: number; - production_code: string; - runtime: number | null; - season_number: number; - show_id: number; - still_path: string | null; - } | null; - name: string; - next_episode_to_air: { - id: number; - name: string; - overview: string; - vote_average: number; - vote_count: number; - air_date: string; - episode_number: number; - production_code: string; - runtime: number | null; - season_number: number; - show_id: number; - still_path: string | null; - } | null; - networks: { - id: number; - logo_path: string; - name: string; - origin_country: string; - }[]; - number_of_episodes: number; - number_of_seasons: number; - origin_country: string[]; - original_language: string; - original_name: string; - overview: string; - popularity: number; - poster_path: string | null; - production_companies: { - id: number; - logo_path: string | null; - name: string; - origin_country: string; - }[]; - production_countries: { - iso_3166_1: string; - name: string; - }[]; - seasons: { - air_date: string; - episode_count: number; - id: number; - name: string; - overview: string; - poster_path: string | null; - season_number: number; - }[]; - spoken_languages: { - english_name: string; - iso_639_1: string; - name: string; - }[]; - status: string; - tagline: string; - type: string; - vote_average: number; - vote_count: number; -} - -export interface TMDBMovieData { - adult: boolean; - backdrop_path: string | null; - belongs_to_collection: { - id: number; - name: string; - poster_path: string | null; - backdrop_path: string | null; - } | null; - budget: number; - genres: { - id: number; - name: string; - }[]; - homepage: string | null; - id: number; - imdb_id: string | null; - original_language: string; - original_title: string; - overview: string | null; - popularity: number; - poster_path: string | null; - production_companies: { - id: number; - logo_path: string | null; - name: string; - origin_country: string; - }[]; - production_countries: { - iso_3166_1: string; - name: string; - }[]; - release_date: string; - revenue: number; - runtime: number | null; - spoken_languages: { - english_name: string; - iso_639_1: string; - name: string; - }[]; - status: string; - tagline: string | null; - title: string; - video: boolean; - vote_average: number; - vote_count: number; -} - -export type TMDBMediaDetailsPromise = Promise; - -export interface TMDBMediaStatic { - getMediaDetails( - id: string, - type: MWMediaType.SERIES - ): TMDBMediaDetailsPromise; - getMediaDetails(id: string, type: MWMediaType.MOVIE): TMDBMediaDetailsPromise; - getMediaDetails(id: string, type: MWMediaType): TMDBMediaDetailsPromise; -} diff --git a/src/backend/metadata/types_old.ts b/src/backend/metadata/types_old.ts new file mode 100644 index 00000000..2723fbe7 --- /dev/null +++ b/src/backend/metadata/types_old.ts @@ -0,0 +1,47 @@ +export enum MWMediaType { + MOVIE = "movie", + SERIES = "series", + ANIME = "anime", +} + +export type MWSeasonMeta = { + id: string; + number: number; + title: string; +}; + +export type MWSeasonWithEpisodeMeta = { + id: string; + number: number; + title: string; + episodes: { + id: string; + number: number; + title: string; + }[]; +}; + +type MWMediaMetaBase = { + title: string; + id: string; + year?: string; + poster?: string; +}; + +type MWMediaMetaSpecific = + | { + type: MWMediaType.MOVIE | MWMediaType.ANIME; + seasons: undefined; + } + | { + type: MWMediaType.SERIES; + seasons: MWSeasonMeta[]; + seasonData: MWSeasonWithEpisodeMeta; + }; + +export type MWMediaMeta = MWMediaMetaBase & MWMediaMetaSpecific; + +export interface MWQuery { + searchQuery: string; + type: MWMediaType; +} diff --git a/src/setup/config.ts b/src/setup/config.ts index f1db01da..c24117bb 100644 --- a/src/setup/config.ts +++ b/src/setup/config.ts @@ -8,6 +8,7 @@ interface Config { TMDB_API_KEY: string; CORS_PROXY_URL: string; NORMAL_ROUTER: boolean; + TRAKT_CLIENT_ID: string; } export interface RuntimeConfig { @@ -18,6 +19,7 @@ export interface RuntimeConfig { TMDB_API_KEY: string; NORMAL_ROUTER: boolean; PROXY_URLS: string[]; + TRAKT_CLIENT_ID: string; } const env: Record = { @@ -28,6 +30,7 @@ const env: Record = { DISCORD_LINK: undefined, CORS_PROXY_URL: import.meta.env.VITE_CORS_PROXY_URL, NORMAL_ROUTER: import.meta.env.VITE_NORMAL_ROUTER, + TRAKT_CLIENT_ID: import.meta.env.VITE_TRAKT_CLIENT_ID, }; const alerts = [] as string[]; @@ -62,5 +65,6 @@ export function conf(): RuntimeConfig { .split(",") .map((v) => v.trim()), NORMAL_ROUTER: getKey("NORMAL_ROUTER", "false") === "true", + TRAKT_CLIENT_ID: getKey("TRAKT_CLIENT_ID"), }; } From c17f8a15e8d8b89a488630b267efc86ab181d04a Mon Sep 17 00:00:00 2001 From: castdrian Date: Mon, 12 Jun 2023 21:25:24 +0200 Subject: [PATCH 31/45] more refactorings --- src/backend/metadata/getmeta.ts | 8 ++--- src/backend/metadata/justwatch.ts | 42 +++++-------------------- src/backend/metadata/search_old.ts | 9 ++---- src/backend/metadata/types.ts | 49 ++++++++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 46 deletions(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index 6b3b9a30..c4771451 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -1,13 +1,13 @@ import { FetchError } from "ofetch"; +import { formatJWMeta, mediaTypeToJW } from "./justwatch"; import { JWMediaResult, JWSeasonMetaResult, JW_API_BASE, - formatJWMeta, - mediaTypeToJW, -} from "./justwatch"; -import { MWMediaMeta, MWMediaType } from "./types"; + MWMediaMeta, + MWMediaType, +} from "./types"; import { makeUrl, proxiedFetch } from "../helpers/fetch"; type JWExternalIdType = diff --git a/src/backend/metadata/justwatch.ts b/src/backend/metadata/justwatch.ts index 5c79c1e3..27c5aa4c 100644 --- a/src/backend/metadata/justwatch.ts +++ b/src/backend/metadata/justwatch.ts @@ -1,38 +1,10 @@ -import { MWMediaMeta, MWMediaType, MWSeasonMeta } from "./types"; - -export const JW_API_BASE = "https://apis.justwatch.com"; -export const JW_IMAGE_BASE = "https://images.justwatch.com"; - -export type JWContentTypes = "movie" | "show"; - -export type JWSeasonShort = { - title: string; - id: number; - season_number: number; -}; - -export type JWEpisodeShort = { - title: string; - id: number; - episode_number: number; -}; - -export type JWMediaResult = { - title: string; - poster?: string; - id: number; - original_release_year?: number; - jw_entity_id: string; - object_type: JWContentTypes; - seasons?: JWSeasonShort[]; -}; - -export type JWSeasonMetaResult = { - title: string; - id: string; - season_number: number; - episodes: JWEpisodeShort[]; -}; +import { + JWContentTypes, + JWMediaResult, + JWSeasonMetaResult, + JW_IMAGE_BASE, +} from "./types"; +import { MWMediaMeta, MWMediaType, MWSeasonMeta } from "./types_old"; export function mediaTypeToJW(type: MWMediaType): JWContentTypes { if (type === MWMediaType.MOVIE) return "movie"; diff --git a/src/backend/metadata/search_old.ts b/src/backend/metadata/search_old.ts index 05be994d..f12f62d2 100644 --- a/src/backend/metadata/search_old.ts +++ b/src/backend/metadata/search_old.ts @@ -1,12 +1,7 @@ import { SimpleCache } from "@/utils/cache"; -import { - JWContentTypes, - JWMediaResult, - JW_API_BASE, - formatJWMeta, - mediaTypeToJW, -} from "./justwatch"; +import { formatJWMeta, mediaTypeToJW } from "./justwatch"; +import { JWContentTypes, JWMediaResult, JW_API_BASE } from "./types"; import { MWMediaMeta, MWQuery } from "./types_old"; import { proxiedFetch } from "../helpers/fetch"; diff --git a/src/backend/metadata/types.ts b/src/backend/metadata/types.ts index f954ff6a..9e87d49d 100644 --- a/src/backend/metadata/types.ts +++ b/src/backend/metadata/types.ts @@ -262,3 +262,52 @@ export interface TMDBMediaStatic { getMediaDetails(id: string, type: MWMediaType.MOVIE): TMDBMediaDetailsPromise; getMediaDetails(id: string, type: MWMediaType): TMDBMediaDetailsPromise; } + +export type JWContentTypes = "movie" | "show"; + +export type JWSearchQuery = { + content_types: JWContentTypes[]; + page: number; + page_size: number; + query: string; +}; + +export type JWPage = { + items: T[]; + page: number; + page_size: number; + total_pages: number; + total_results: number; +}; + +export const JW_API_BASE = "https://apis.justwatch.com"; +export const JW_IMAGE_BASE = "https://images.justwatch.com"; + +export type JWSeasonShort = { + title: string; + id: number; + season_number: number; +}; + +export type JWEpisodeShort = { + title: string; + id: number; + episode_number: number; +}; + +export type JWMediaResult = { + title: string; + poster?: string; + id: number; + original_release_year?: number; + jw_entity_id: string; + object_type: JWContentTypes; + seasons?: JWSeasonShort[]; +}; + +export type JWSeasonMetaResult = { + title: string; + id: string; + season_number: number; + episodes: JWEpisodeShort[]; +}; From 3af98373fbafc7d979492fb87d21033bd2241890 Mon Sep 17 00:00:00 2001 From: castdrian Date: Tue, 13 Jun 2023 10:41:54 +0200 Subject: [PATCH 32/45] finish initial refactor --- src/backend/metadata/getmeta.ts | 55 +++++++++++++++++++++++++++++++++ src/backend/metadata/search.ts | 1 + src/backend/metadata/tmdb.ts | 27 ---------------- src/backend/metadata/trakttv.ts | 45 ++++++++++++++++++++------- src/backend/metadata/types.ts | 12 +++++++ 5 files changed, 101 insertions(+), 39 deletions(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index c4771451..e428199e 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -1,12 +1,17 @@ import { FetchError } from "ofetch"; import { formatJWMeta, mediaTypeToJW } from "./justwatch"; +import { Tmdb } from "./tmdb"; +import { Trakt, formatTTVMeta } from "./trakttv"; import { JWMediaResult, JWSeasonMetaResult, JW_API_BASE, MWMediaMeta, MWMediaType, + TMDBMovieData, + TMDBShowData, + TTVSeasonMetaResult, } from "./types"; import { makeUrl, proxiedFetch } from "../helpers/fetch"; @@ -37,6 +42,56 @@ export async function getMetaFromId( type: MWMediaType, id: string, seasonId?: string +): Promise { + const result = await Trakt.searchById(id, mediaTypeToJW(type)); + if (!result) return null; + const details = await Tmdb.getMediaDetails(id, type); + + if (!details) return null; + + let imdbId; + if (type === MWMediaType.MOVIE) { + imdbId = (details as TMDBMovieData).imdb_id ?? undefined; + } + + let seasonData: TTVSeasonMetaResult | undefined; + + if (type === MWMediaType.SERIES) { + const seasons = (details as TMDBShowData).seasons; + const season = + seasons?.find((v) => v.id.toString() === seasonId) ?? seasons?.[0]; + + const episodes = await Trakt.getEpisodes( + result.ttv_entity_id, + season?.season_number ?? 1 + ); + + if (season && episodes) { + seasonData = { + id: season.id.toString(), + season_number: season.season_number, + title: season.name, + episodes, + }; + } + } + + const meta = formatTTVMeta(result, seasonData); + if (!meta) return null; + + console.log(meta); + + return { + meta, + imdbId, + tmdbId: id, + }; +} + +export async function getLegacyMetaFromId( + type: MWMediaType, + id: string, + seasonId?: string ): Promise { const queryType = mediaTypeToJW(type); diff --git a/src/backend/metadata/search.ts b/src/backend/metadata/search.ts index 4506514a..8eb246b7 100644 --- a/src/backend/metadata/search.ts +++ b/src/backend/metadata/search.ts @@ -16,6 +16,7 @@ export async function searchForMedia(query: MWQuery): Promise { const contentType = mediaTypeToTTV(type); const results = await Trakt.search(searchQuery, contentType); + console.log(results[0]); cache.set(query, results, 3600); return results; } diff --git a/src/backend/metadata/tmdb.ts b/src/backend/metadata/tmdb.ts index 460e13b4..3aa1821f 100644 --- a/src/backend/metadata/tmdb.ts +++ b/src/backend/metadata/tmdb.ts @@ -47,31 +47,4 @@ export abstract class Tmdb { public static getMediaPoster(posterPath: string | null): string | undefined { if (posterPath) return `https://image.tmdb.org/t/p/w185/${posterPath}`; } - - /* public static async getMetaFromId( - type: MWMediaType, - id: string, - seasonId?: string - ): Promise { - console.log("getMetaFromId", type, id, seasonId); - - const details = await Tmdb.getMediaDetails(id, type); - - if (!details) return null; - - let imdbId; - if (type === MWMediaType.MOVIE) { - imdbId = (details as TMDBMovieData).imdb_id ?? undefined; - } - - if (!meta.length) return null; - - console.log(meta); - - return { - meta, - imdbId, - tmdbId: id, - }; - } */ } diff --git a/src/backend/metadata/trakttv.ts b/src/backend/metadata/trakttv.ts index 5fb67a17..ae50aefe 100644 --- a/src/backend/metadata/trakttv.ts +++ b/src/backend/metadata/trakttv.ts @@ -2,12 +2,13 @@ import { conf } from "@/setup/config"; import { Tmdb } from "./tmdb"; import { - DetailedMeta, MWMediaMeta, MWMediaType, MWSeasonMeta, TMDBShowData, TTVContentTypes, + TTVEpisodeResult, + TTVEpisodeShort, TTVMediaResult, TTVSearchResult, TTVSeasonMetaResult, @@ -69,14 +70,14 @@ export function formatTTVMeta( } export function TTVMediaToId(media: MWMediaMeta): string { - return ["TTV", mediaTypeToTTV(media.type), media.id].join("-"); + return ["MW", mediaTypeToTTV(media.type), media.id].join("-"); } export function decodeTTVId( paramId: string ): { id: string; type: MWMediaType } | null { const [prefix, type, id] = paramId.split("-", 3); - if (prefix !== "TTV") return null; + if (prefix !== "MW") return null; let mediaType; try { mediaType = TTVMediaToMediaType(type); @@ -101,7 +102,6 @@ export async function formatTTVSearchResult( media.ids.tmdb.toString(), TTVMediaToMediaType(result.type) ); - console.log(details); const seasons = type === MWMediaType.SERIES @@ -115,7 +115,7 @@ export async function formatTTVSearchResult( return { title: media.title, poster: Tmdb.getMediaPoster(details.poster_path), - id: media.ids.trakt, + id: media.ids.tmdb, original_release_year: media.year, ttv_entity_id: media.ids.slug, object_type: mediaTypeToTTV(type), @@ -155,12 +155,33 @@ export abstract class Trakt { return formatted.map((v) => formatTTVMeta(v)); } - public static async getMetaFromId( - type: MWMediaType, - id: string, - seasonId?: string - ): Promise { - console.log("getMetaFromId", type, id, seasonId); - return null; + public static async searchById( + tmdbId: string, + type: "movie" | "show" + ): Promise { + const data = await Trakt.get( + `/search/tmdb/${tmdbId}?type=${type}` + ); + + const formatted = await Promise.all( + // eslint-disable-next-line no-return-await + data.map(async (v) => await formatTTVSearchResult(v)) + ); + return formatted[0]; + } + + public static async getEpisodes( + slug: string, + season: number + ): Promise { + const data = await Trakt.get( + `/shows/${slug}/seasons/${season}` + ); + + return data.map((e) => ({ + id: e.ids.tmdb, + episode_number: e.number, + title: e.title, + })); } } diff --git a/src/backend/metadata/types.ts b/src/backend/metadata/types.ts index 9e87d49d..07671e39 100644 --- a/src/backend/metadata/types.ts +++ b/src/backend/metadata/types.ts @@ -311,3 +311,15 @@ export type JWSeasonMetaResult = { season_number: number; episodes: JWEpisodeShort[]; }; + +export interface TTVEpisodeResult { + season: number; + number: number; + title: string; + ids: { + trakt: number; + tvdb: number; + imdb: string; + tmdb: number; + }; +} From 70f835538683e3dba953b443a2b17538e9567764 Mon Sep 17 00:00:00 2001 From: castdrian Date: Tue, 13 Jun 2023 11:01:07 +0200 Subject: [PATCH 33/45] refactor url prefix --- src/backend/metadata/getmeta.ts | 28 ++++++++++++++++++- src/components/media/MediaCard.tsx | 4 +-- .../popouts/EpisodeSelectionPopout.tsx | 5 ++-- src/views/media/MediaView.tsx | 9 ++++-- 4 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index e428199e..26464299 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -2,7 +2,12 @@ import { FetchError } from "ofetch"; import { formatJWMeta, mediaTypeToJW } from "./justwatch"; import { Tmdb } from "./tmdb"; -import { Trakt, formatTTVMeta } from "./trakttv"; +import { + TTVMediaToMediaType, + Trakt, + formatTTVMeta, + mediaTypeToTTV, +} from "./trakttv"; import { JWMediaResult, JWSeasonMetaResult, @@ -137,3 +142,24 @@ export async function getLegacyMetaFromId( tmdbId, }; } + +export function MWMediaToId(media: MWMediaMeta): string { + return ["MW", mediaTypeToTTV(media.type), media.id].join("-"); +} + +export function decodeMWId( + paramId: string +): { id: string; type: MWMediaType } | null { + const [prefix, type, id] = paramId.split("-", 3); + if (prefix !== "MW") return null; + let mediaType; + try { + mediaType = TTVMediaToMediaType(type); + } catch { + return null; + } + return { + type: mediaType, + id, + }; +} diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index 22865717..3bac4d08 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -1,7 +1,7 @@ import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; -import { JWMediaToId } from "@/backend/metadata/justwatch"; +import { MWMediaToId } from "@/backend/metadata/getmeta"; import { MWMediaMeta } from "@/backend/metadata/types"; import { DotList } from "@/components/text/DotList"; @@ -132,7 +132,7 @@ export function MediaCard(props: MediaCardProps) { const canLink = props.linkable && !props.closable; let link = canLink - ? `/media/${encodeURIComponent(JWMediaToId(props.media))}` + ? `/media/${encodeURIComponent(MWMediaToId(props.media))}` : "#"; if (canLink && props.series) link += `/${encodeURIComponent(props.series.seasonId)}/${encodeURIComponent( diff --git a/src/video/components/popouts/EpisodeSelectionPopout.tsx b/src/video/components/popouts/EpisodeSelectionPopout.tsx index c80045bd..3d0b431b 100644 --- a/src/video/components/popouts/EpisodeSelectionPopout.tsx +++ b/src/video/components/popouts/EpisodeSelectionPopout.tsx @@ -2,8 +2,7 @@ import { useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useParams } from "react-router-dom"; -import { getMetaFromId } from "@/backend/metadata/getmeta"; -import { decodeJWId } from "@/backend/metadata/justwatch"; +import { decodeMWId, getMetaFromId } from "@/backend/metadata/getmeta"; import { MWMediaType, MWSeasonWithEpisodeMeta } from "@/backend/metadata/types"; import { IconPatch } from "@/components/buttons/IconPatch"; import { Icon, Icons } from "@/components/Icon"; @@ -45,7 +44,7 @@ export function EpisodeSelectionPopout() { seasonId: sId, season: undefined, }); - reqSeasonMeta(decodeJWId(params.media)?.id as string, sId).then((v) => { + reqSeasonMeta(decodeMWId(params.media)?.id as string, sId).then((v) => { if (v?.meta.type !== MWMediaType.SERIES) return; setCurrentVisibleSeason({ seasonId: sId, diff --git a/src/views/media/MediaView.tsx b/src/views/media/MediaView.tsx index c55211c7..1a4709e3 100644 --- a/src/views/media/MediaView.tsx +++ b/src/views/media/MediaView.tsx @@ -4,8 +4,11 @@ import { useTranslation } from "react-i18next"; import { useHistory, useParams } from "react-router-dom"; import { MWStream } from "@/backend/helpers/streams"; -import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta"; -import { decodeJWId } from "@/backend/metadata/justwatch"; +import { + DetailedMeta, + decodeMWId, + getMetaFromId, +} from "@/backend/metadata/getmeta"; import { MWMediaType, MWSeasonWithEpisodeMeta } from "@/backend/metadata/types"; import { IconPatch } from "@/components/buttons/IconPatch"; import { Icons } from "@/components/Icon"; @@ -181,7 +184,7 @@ export function MediaView() { const [selected, setSelected] = useState(null); const [exec, loading, error] = useLoading( async (mediaParams: string, seasonId?: string) => { - const data = decodeJWId(mediaParams); + const data = decodeMWId(mediaParams); if (!data) return null; return getMetaFromId(data.type, data.id, seasonId); } From 879271c23976126b41a73f439f01cacc5140edbf Mon Sep 17 00:00:00 2001 From: castdrian Date: Tue, 13 Jun 2023 14:06:37 +0200 Subject: [PATCH 34/45] implement legacy url conversion --- src/backend/metadata/getmeta.ts | 15 +++++++++++++++ src/setup/App.tsx | 18 +++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index 26464299..b6f90f26 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -163,3 +163,18 @@ export function decodeMWId( id, }; } + +export async function convertLegacyUrl( + url: string +): Promise { + if (url.startsWith("/media/JW")) { + const urlParts = url.split("/").slice(2); + const [, type, id] = urlParts[0].split("-", 3); + const meta = await getLegacyMetaFromId(TTVMediaToMediaType(type), id); + if (!meta) return undefined; + const tmdbId = meta.tmdbId; + if (!tmdbId) return undefined; + return `/media/MW-${type}-${tmdbId}`; + } + return undefined; +} diff --git a/src/setup/App.tsx b/src/setup/App.tsx index 992549e0..7be4d581 100644 --- a/src/setup/App.tsx +++ b/src/setup/App.tsx @@ -1,6 +1,13 @@ import { lazy } from "react"; -import { Redirect, Route, Switch } from "react-router-dom"; +import { + Redirect, + Route, + Switch, + useHistory, + useLocation, +} from "react-router-dom"; +import { convertLegacyUrl } from "@/backend/metadata/getmeta"; import { MWMediaType } from "@/backend/metadata/types"; import { BannerContextProvider } from "@/hooks/useBanner"; import { Layout } from "@/setup/Layout"; @@ -13,6 +20,15 @@ import { V2MigrationView } from "@/views/other/v2Migration"; import { SearchView } from "@/views/search/SearchView"; function App() { + const location = useLocation(); + const history = useHistory(); + + // Call the conversion function and redirect if necessary + convertLegacyUrl(location.pathname).then((convertedUrl) => { + if (convertedUrl) { + history.replace(convertedUrl); + } + }); return ( From b5c330d4e3cd4b4aa77b3f3dc7cfcba02a785f7b Mon Sep 17 00:00:00 2001 From: castdrian Date: Tue, 13 Jun 2023 14:20:33 +0200 Subject: [PATCH 35/45] refactor to initial prefix choice --- src/backend/metadata/getmeta.ts | 10 +++++----- src/components/media/MediaCard.tsx | 4 ++-- .../components/popouts/EpisodeSelectionPopout.tsx | 4 ++-- src/views/media/MediaView.tsx | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index b6f90f26..82b3c20b 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -143,15 +143,15 @@ export async function getLegacyMetaFromId( }; } -export function MWMediaToId(media: MWMediaMeta): string { - return ["MW", mediaTypeToTTV(media.type), media.id].join("-"); +export function TTVMediaToId(media: MWMediaMeta): string { + return ["TTV", mediaTypeToTTV(media.type), media.id].join("-"); } -export function decodeMWId( +export function decodeTTVId( paramId: string ): { id: string; type: MWMediaType } | null { const [prefix, type, id] = paramId.split("-", 3); - if (prefix !== "MW") return null; + if (prefix !== "TTV") return null; let mediaType; try { mediaType = TTVMediaToMediaType(type); @@ -174,7 +174,7 @@ export async function convertLegacyUrl( if (!meta) return undefined; const tmdbId = meta.tmdbId; if (!tmdbId) return undefined; - return `/media/MW-${type}-${tmdbId}`; + return `/media/TTV-${type}-${tmdbId}`; } return undefined; } diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index 3bac4d08..b87654a9 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -1,7 +1,7 @@ import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; -import { MWMediaToId } from "@/backend/metadata/getmeta"; +import { TTVMediaToId } from "@/backend/metadata/getmeta"; import { MWMediaMeta } from "@/backend/metadata/types"; import { DotList } from "@/components/text/DotList"; @@ -132,7 +132,7 @@ export function MediaCard(props: MediaCardProps) { const canLink = props.linkable && !props.closable; let link = canLink - ? `/media/${encodeURIComponent(MWMediaToId(props.media))}` + ? `/media/${encodeURIComponent(TTVMediaToId(props.media))}` : "#"; if (canLink && props.series) link += `/${encodeURIComponent(props.series.seasonId)}/${encodeURIComponent( diff --git a/src/video/components/popouts/EpisodeSelectionPopout.tsx b/src/video/components/popouts/EpisodeSelectionPopout.tsx index 3d0b431b..7ea6c5a3 100644 --- a/src/video/components/popouts/EpisodeSelectionPopout.tsx +++ b/src/video/components/popouts/EpisodeSelectionPopout.tsx @@ -2,7 +2,7 @@ import { useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useParams } from "react-router-dom"; -import { decodeMWId, getMetaFromId } from "@/backend/metadata/getmeta"; +import { decodeTTVId, getMetaFromId } from "@/backend/metadata/getmeta"; import { MWMediaType, MWSeasonWithEpisodeMeta } from "@/backend/metadata/types"; import { IconPatch } from "@/components/buttons/IconPatch"; import { Icon, Icons } from "@/components/Icon"; @@ -44,7 +44,7 @@ export function EpisodeSelectionPopout() { seasonId: sId, season: undefined, }); - reqSeasonMeta(decodeMWId(params.media)?.id as string, sId).then((v) => { + reqSeasonMeta(decodeTTVId(params.media)?.id as string, sId).then((v) => { if (v?.meta.type !== MWMediaType.SERIES) return; setCurrentVisibleSeason({ seasonId: sId, diff --git a/src/views/media/MediaView.tsx b/src/views/media/MediaView.tsx index 1a4709e3..c0e81455 100644 --- a/src/views/media/MediaView.tsx +++ b/src/views/media/MediaView.tsx @@ -6,7 +6,7 @@ import { useHistory, useParams } from "react-router-dom"; import { MWStream } from "@/backend/helpers/streams"; import { DetailedMeta, - decodeMWId, + decodeTTVId, getMetaFromId, } from "@/backend/metadata/getmeta"; import { MWMediaType, MWSeasonWithEpisodeMeta } from "@/backend/metadata/types"; @@ -184,7 +184,7 @@ export function MediaView() { const [selected, setSelected] = useState(null); const [exec, loading, error] = useLoading( async (mediaParams: string, seasonId?: string) => { - const data = decodeMWId(mediaParams); + const data = decodeTTVId(mediaParams); if (!data) return null; return getMetaFromId(data.type, data.id, seasonId); } From 8da155ba2bfad94160469118804665e34a80c8a1 Mon Sep 17 00:00:00 2001 From: castdrian Date: Tue, 13 Jun 2023 14:25:31 +0200 Subject: [PATCH 36/45] cleanup --- src/backend/metadata/justwatch.ts | 4 ++- src/backend/metadata/search_old.ts | 54 ------------------------------ src/backend/metadata/types_old.ts | 47 -------------------------- 3 files changed, 3 insertions(+), 102 deletions(-) delete mode 100644 src/backend/metadata/search_old.ts delete mode 100644 src/backend/metadata/types_old.ts diff --git a/src/backend/metadata/justwatch.ts b/src/backend/metadata/justwatch.ts index 27c5aa4c..857ff006 100644 --- a/src/backend/metadata/justwatch.ts +++ b/src/backend/metadata/justwatch.ts @@ -3,8 +3,10 @@ import { JWMediaResult, JWSeasonMetaResult, JW_IMAGE_BASE, + MWMediaMeta, + MWMediaType, + MWSeasonMeta, } from "./types"; -import { MWMediaMeta, MWMediaType, MWSeasonMeta } from "./types_old"; export function mediaTypeToJW(type: MWMediaType): JWContentTypes { if (type === MWMediaType.MOVIE) return "movie"; diff --git a/src/backend/metadata/search_old.ts b/src/backend/metadata/search_old.ts deleted file mode 100644 index f12f62d2..00000000 --- a/src/backend/metadata/search_old.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { SimpleCache } from "@/utils/cache"; - -import { formatJWMeta, mediaTypeToJW } from "./justwatch"; -import { JWContentTypes, JWMediaResult, JW_API_BASE } from "./types"; -import { MWMediaMeta, MWQuery } from "./types_old"; -import { proxiedFetch } from "../helpers/fetch"; - -const cache = new SimpleCache(); -cache.setCompare((a, b) => { - return a.type === b.type && a.searchQuery.trim() === b.searchQuery.trim(); -}); -cache.initialize(); - -type JWSearchQuery = { - content_types: JWContentTypes[]; - page: number; - page_size: number; - query: string; -}; - -type JWPage = { - items: T[]; - page: number; - page_size: number; - total_pages: number; - total_results: number; -}; - -export async function searchForMedia(query: MWQuery): Promise { - if (cache.has(query)) return cache.get(query) as MWMediaMeta[]; - const { searchQuery, type } = query; - - const contentType = mediaTypeToJW(type); - const body: JWSearchQuery = { - content_types: [contentType], - page: 1, - query: searchQuery, - page_size: 40, - }; - - const data = await proxiedFetch>( - "/content/titles/en_US/popular", - { - baseURL: JW_API_BASE, - params: { - body: JSON.stringify(body), - }, - } - ); - - const returnData = data.items.map((v) => formatJWMeta(v)); - cache.set(query, returnData, 3600); // cache for an hour - return returnData; -} diff --git a/src/backend/metadata/types_old.ts b/src/backend/metadata/types_old.ts deleted file mode 100644 index 2723fbe7..00000000 --- a/src/backend/metadata/types_old.ts +++ /dev/null @@ -1,47 +0,0 @@ -export enum MWMediaType { - MOVIE = "movie", - SERIES = "series", - ANIME = "anime", -} - -export type MWSeasonMeta = { - id: string; - number: number; - title: string; -}; - -export type MWSeasonWithEpisodeMeta = { - id: string; - number: number; - title: string; - episodes: { - id: string; - number: number; - title: string; - }[]; -}; - -type MWMediaMetaBase = { - title: string; - id: string; - year?: string; - poster?: string; -}; - -type MWMediaMetaSpecific = - | { - type: MWMediaType.MOVIE | MWMediaType.ANIME; - seasons: undefined; - } - | { - type: MWMediaType.SERIES; - seasons: MWSeasonMeta[]; - seasonData: MWSeasonWithEpisodeMeta; - }; - -export type MWMediaMeta = MWMediaMetaBase & MWMediaMetaSpecific; - -export interface MWQuery { - searchQuery: string; - type: MWMediaType; -} From 46bd20f71866241a09756d46b111f1ef416fe02b Mon Sep 17 00:00:00 2001 From: castdrian Date: Tue, 13 Jun 2023 21:23:47 +0200 Subject: [PATCH 37/45] refactor everything to use tmdb exclusively --- src/backend/metadata/getmeta.ts | 61 +++--- src/backend/metadata/search.ts | 20 +- src/backend/metadata/tmdb.ts | 159 ++++++++++++++- src/backend/metadata/trakttv.ts | 187 ------------------ src/backend/metadata/types.ts | 140 +++++++++---- src/components/media/MediaCard.tsx | 4 +- .../popouts/EpisodeSelectionPopout.tsx | 4 +- src/views/media/MediaView.tsx | 4 +- 8 files changed, 315 insertions(+), 264 deletions(-) delete mode 100644 src/backend/metadata/trakttv.ts diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index 82b3c20b..777fae42 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -1,22 +1,22 @@ import { FetchError } from "ofetch"; import { formatJWMeta, mediaTypeToJW } from "./justwatch"; -import { Tmdb } from "./tmdb"; import { - TTVMediaToMediaType, - Trakt, - formatTTVMeta, - mediaTypeToTTV, -} from "./trakttv"; + TMDBMediaToMediaType, + Tmdb, + formatTMDBMeta, + mediaTypeToTMDB, +} from "./tmdb"; import { JWMediaResult, JWSeasonMetaResult, JW_API_BASE, MWMediaMeta, MWMediaType, + TMDBMediaResult, TMDBMovieData, + TMDBSeasonMetaResult, TMDBShowData, - TTVSeasonMetaResult, } from "./types"; import { makeUrl, proxiedFetch } from "../helpers/fetch"; @@ -48,9 +48,7 @@ export async function getMetaFromId( id: string, seasonId?: string ): Promise { - const result = await Trakt.searchById(id, mediaTypeToJW(type)); - if (!result) return null; - const details = await Tmdb.getMediaDetails(id, type); + const details = await Tmdb.getMediaDetails(id, mediaTypeToTMDB(type)); if (!details) return null; @@ -59,15 +57,15 @@ export async function getMetaFromId( imdbId = (details as TMDBMovieData).imdb_id ?? undefined; } - let seasonData: TTVSeasonMetaResult | undefined; + let seasonData: TMDBSeasonMetaResult | undefined; if (type === MWMediaType.SERIES) { const seasons = (details as TMDBShowData).seasons; const season = seasons?.find((v) => v.id.toString() === seasonId) ?? seasons?.[0]; - const episodes = await Trakt.getEpisodes( - result.ttv_entity_id, + const episodes = await Tmdb.getEpisodes( + details.id.toString(), season?.season_number ?? 1 ); @@ -81,10 +79,27 @@ export async function getMetaFromId( } } - const meta = formatTTVMeta(result, seasonData); - if (!meta) return null; + const tmdbmeta: TMDBMediaResult = { + id: details.id, + title: + type === MWMediaType.MOVIE + ? (details as TMDBMovieData).title + : (details as TMDBShowData).name, + object_type: mediaTypeToTMDB(type), + seasons: (details as TMDBShowData).seasons.map((v) => ({ + id: v.id, + season_number: v.season_number, + title: v.name, + })), + poster: (details as TMDBMovieData).poster_path ?? undefined, + original_release_year: + type === MWMediaType.MOVIE + ? Number((details as TMDBMovieData).release_date?.split("-")[0]) + : Number((details as TMDBShowData).first_air_date?.split("-")[0]), + }; - console.log(meta); + const meta = formatTMDBMeta(tmdbmeta, seasonData); + if (!meta) return null; return { meta, @@ -143,18 +158,18 @@ export async function getLegacyMetaFromId( }; } -export function TTVMediaToId(media: MWMediaMeta): string { - return ["TTV", mediaTypeToTTV(media.type), media.id].join("-"); +export function TMDBMediaToId(media: MWMediaMeta): string { + return ["tmdb", mediaTypeToTMDB(media.type), media.id].join("-"); } -export function decodeTTVId( +export function decodeTMDBId( paramId: string ): { id: string; type: MWMediaType } | null { const [prefix, type, id] = paramId.split("-", 3); - if (prefix !== "TTV") return null; + if (prefix !== "tmdb") return null; let mediaType; try { - mediaType = TTVMediaToMediaType(type); + mediaType = TMDBMediaToMediaType(type); } catch { return null; } @@ -170,11 +185,11 @@ export async function convertLegacyUrl( if (url.startsWith("/media/JW")) { const urlParts = url.split("/").slice(2); const [, type, id] = urlParts[0].split("-", 3); - const meta = await getLegacyMetaFromId(TTVMediaToMediaType(type), id); + const meta = await getLegacyMetaFromId(TMDBMediaToMediaType(type), id); if (!meta) return undefined; const tmdbId = meta.tmdbId; if (!tmdbId) return undefined; - return `/media/TTV-${type}-${tmdbId}`; + return `/media/tmdb-${type}-${tmdbId}`; } return undefined; } diff --git a/src/backend/metadata/search.ts b/src/backend/metadata/search.ts index 8eb246b7..31b2c682 100644 --- a/src/backend/metadata/search.ts +++ b/src/backend/metadata/search.ts @@ -1,6 +1,11 @@ import { SimpleCache } from "@/utils/cache"; -import { Trakt, mediaTypeToTTV } from "./trakttv"; +import { + Tmdb, + formatTMDBMeta, + formatTMDBSearchResult, + mediaTypeToTMDB, +} from "./tmdb"; import { MWMediaMeta, MWQuery } from "./types"; const cache = new SimpleCache(); @@ -13,10 +18,17 @@ export async function searchForMedia(query: MWQuery): Promise { if (cache.has(query)) return cache.get(query) as MWMediaMeta[]; const { searchQuery, type } = query; - const contentType = mediaTypeToTTV(type); + const data = await Tmdb.searchMedia(searchQuery, mediaTypeToTMDB(type)); + const results = await Promise.all( + data.results.map(async (v) => { + const formattedResult = await formatTMDBSearchResult( + v, + mediaTypeToTMDB(type) + ); + return formatTMDBMeta(formattedResult); + }) + ); - const results = await Trakt.search(searchQuery, contentType); - console.log(results[0]); cache.set(query, results, 3600); return results; } diff --git a/src/backend/metadata/tmdb.ts b/src/backend/metadata/tmdb.ts index 3aa1821f..f01709bb 100644 --- a/src/backend/metadata/tmdb.ts +++ b/src/backend/metadata/tmdb.ts @@ -1,13 +1,100 @@ import { conf } from "@/setup/config"; import { + MWMediaMeta, MWMediaType, + MWSeasonMeta, + TMDBContentTypes, + TMDBEpisodeShort, + TMDBMediaResult, TMDBMediaStatic, TMDBMovieData, + TMDBMovieResponse, + TMDBMovieResult, + TMDBSearchResultStatic, + TMDBSeason, + TMDBSeasonMetaResult, TMDBShowData, + TMDBShowResponse, + TMDBShowResult, } from "./types"; import { mwFetch } from "../helpers/fetch"; +export function mediaTypeToTMDB(type: MWMediaType): TMDBContentTypes { + if (type === MWMediaType.MOVIE) return "movie"; + if (type === MWMediaType.SERIES) return "show"; + throw new Error("unsupported type"); +} + +export function TMDBMediaToMediaType(type: string): MWMediaType { + if (type === "movie") return MWMediaType.MOVIE; + if (type === "show") return MWMediaType.SERIES; + throw new Error("unsupported type"); +} + +export function formatTMDBMeta( + media: TMDBMediaResult, + season?: TMDBSeasonMetaResult +): MWMediaMeta { + const type = TMDBMediaToMediaType(media.object_type); + let seasons: undefined | MWSeasonMeta[]; + if (type === MWMediaType.SERIES) { + seasons = media.seasons + ?.sort((a, b) => a.season_number - b.season_number) + .map( + (v): MWSeasonMeta => ({ + title: v.title, + id: v.id.toString(), + number: v.season_number, + }) + ); + } + + return { + title: media.title, + id: media.id.toString(), + year: media.original_release_year?.toString(), + poster: media.poster, + type, + seasons: seasons as any, + seasonData: season + ? ({ + id: season.id.toString(), + number: season.season_number, + title: season.title, + episodes: season.episodes + .sort((a, b) => a.episode_number - b.episode_number) + .map((v) => ({ + id: v.id.toString(), + number: v.episode_number, + title: v.title, + })), + } as any) + : (undefined as any), + }; +} + +export function TMDBMediaToId(media: MWMediaMeta): string { + return ["tmdb", mediaTypeToTMDB(media.type), media.id].join("-"); +} + +export function decodeTMDBId( + paramId: string +): { id: string; type: MWMediaType } | null { + const [prefix, type, id] = paramId.split("-", 3); + if (prefix !== "tmdb") return null; + let mediaType; + try { + mediaType = TMDBMediaToMediaType(type); + } catch { + return null; + } + return { + type: mediaType, + id, + }; +} + export abstract class Tmdb { private static baseURL = "https://api.themoviedb.org/3"; @@ -24,9 +111,33 @@ export abstract class Tmdb { return res; } + public static searchMedia: TMDBSearchResultStatic["searchMedia"] = async ( + query: string, + type: TMDBContentTypes + ) => { + let data; + + switch (type) { + case "movie": + data = await Tmdb.get( + `search/movie?query=${query}&include_adult=true&language=en-US&page=1` + ); + break; + case "show": + data = await Tmdb.get( + `search/tv?query=${query}&include_adult=true&language=en-US&page=1` + ); + break; + default: + throw new Error("Invalid media type"); + } + + return data; + }; + public static getMediaDetails: TMDBMediaStatic["getMediaDetails"] = async ( id: string, - type: MWMediaType + type: TMDBContentTypes ) => { let data; @@ -34,7 +145,7 @@ export abstract class Tmdb { case "movie": data = await Tmdb.get(`/movie/${id}`); break; - case "series": + case "show": data = await Tmdb.get(`/tv/${id}`); break; default: @@ -47,4 +158,48 @@ export abstract class Tmdb { public static getMediaPoster(posterPath: string | null): string | undefined { if (posterPath) return `https://image.tmdb.org/t/p/w185/${posterPath}`; } + + public static async getEpisodes( + id: string, + season: number + ): Promise { + const data = await Tmdb.get(`/tv/${id}/season/${season}`); + return data.episodes.map((e) => ({ + id: e.id, + episode_number: e.episode_number, + title: e.name, + })); + } +} + +export async function formatTMDBSearchResult( + result: TMDBShowResult | TMDBMovieResult, + mediatype: TMDBContentTypes +): Promise { + const type = TMDBMediaToMediaType(mediatype); + const details = await Tmdb.getMediaDetails(result.id.toString(), mediatype); + + const seasons = + type === MWMediaType.SERIES + ? (details as TMDBShowData).seasons?.map((v) => ({ + id: v.id, + title: v.name, + season_number: v.season_number, + })) + : undefined; + + return { + title: + type === MWMediaType.SERIES + ? (result as TMDBShowResult).name + : (result as TMDBMovieResult).title, + poster: Tmdb.getMediaPoster(details.poster_path), + id: result.id, + original_release_year: + type === MWMediaType.SERIES + ? Number((result as TMDBShowResult).first_air_date?.split("-")[0]) + : Number((result as TMDBMovieResult).release_date?.split("-")[0]), + object_type: mediaTypeToTMDB(type), + seasons, + }; } diff --git a/src/backend/metadata/trakttv.ts b/src/backend/metadata/trakttv.ts deleted file mode 100644 index ae50aefe..00000000 --- a/src/backend/metadata/trakttv.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { conf } from "@/setup/config"; - -import { Tmdb } from "./tmdb"; -import { - MWMediaMeta, - MWMediaType, - MWSeasonMeta, - TMDBShowData, - TTVContentTypes, - TTVEpisodeResult, - TTVEpisodeShort, - TTVMediaResult, - TTVSearchResult, - TTVSeasonMetaResult, -} from "./types"; -import { mwFetch } from "../helpers/fetch"; - -export function mediaTypeToTTV(type: MWMediaType): TTVContentTypes { - if (type === MWMediaType.MOVIE) return "movie"; - if (type === MWMediaType.SERIES) return "show"; - throw new Error("unsupported type"); -} - -export function TTVMediaToMediaType(type: string): MWMediaType { - if (type === "movie") return MWMediaType.MOVIE; - if (type === "show") return MWMediaType.SERIES; - throw new Error("unsupported type"); -} - -export function formatTTVMeta( - media: TTVMediaResult, - season?: TTVSeasonMetaResult -): MWMediaMeta { - const type = TTVMediaToMediaType(media.object_type); - let seasons: undefined | MWSeasonMeta[]; - if (type === MWMediaType.SERIES) { - seasons = media.seasons - ?.sort((a, b) => a.season_number - b.season_number) - .map( - (v): MWSeasonMeta => ({ - title: v.title, - id: v.id.toString(), - number: v.season_number, - }) - ); - } - - return { - title: media.title, - id: media.id.toString(), - year: media.original_release_year?.toString(), - poster: media.poster, - type, - seasons: seasons as any, - seasonData: season - ? ({ - id: season.id.toString(), - number: season.season_number, - title: season.title, - episodes: season.episodes - .sort((a, b) => a.episode_number - b.episode_number) - .map((v) => ({ - id: v.id.toString(), - number: v.episode_number, - title: v.title, - })), - } as any) - : (undefined as any), - }; -} - -export function TTVMediaToId(media: MWMediaMeta): string { - return ["MW", mediaTypeToTTV(media.type), media.id].join("-"); -} - -export function decodeTTVId( - paramId: string -): { id: string; type: MWMediaType } | null { - const [prefix, type, id] = paramId.split("-", 3); - if (prefix !== "MW") return null; - let mediaType; - try { - mediaType = TTVMediaToMediaType(type); - } catch { - return null; - } - return { - type: mediaType, - id, - }; -} - -export async function formatTTVSearchResult( - result: TTVSearchResult -): Promise { - const type = TTVMediaToMediaType(result.type); - const media = result[result.type]; - - if (!media) throw new Error("invalid result"); - - const details = await Tmdb.getMediaDetails( - media.ids.tmdb.toString(), - TTVMediaToMediaType(result.type) - ); - - const seasons = - type === MWMediaType.SERIES - ? (details as TMDBShowData).seasons?.map((v) => ({ - id: v.id, - title: v.name, - season_number: v.season_number, - })) - : undefined; - - return { - title: media.title, - poster: Tmdb.getMediaPoster(details.poster_path), - id: media.ids.tmdb, - original_release_year: media.year, - ttv_entity_id: media.ids.slug, - object_type: mediaTypeToTTV(type), - seasons, - }; -} - -export abstract class Trakt { - private static baseURL = "https://api.trakt.tv"; - - private static headers = { - "Content-Type": "application/json", - "trakt-api-version": "2", - "trakt-api-key": conf().TRAKT_CLIENT_ID, - }; - - private static async get(url: string): Promise { - const res = await mwFetch(url, { - headers: Trakt.headers, - baseURL: Trakt.baseURL, - }); - return res; - } - - public static async search( - query: string, - type: "movie" | "show" - ): Promise { - const data = await Trakt.get( - `/search/${type}?query=${encodeURIComponent(query)}` - ); - - const formatted = await Promise.all( - // eslint-disable-next-line no-return-await - data.map(async (v) => await formatTTVSearchResult(v)) - ); - return formatted.map((v) => formatTTVMeta(v)); - } - - public static async searchById( - tmdbId: string, - type: "movie" | "show" - ): Promise { - const data = await Trakt.get( - `/search/tmdb/${tmdbId}?type=${type}` - ); - - const formatted = await Promise.all( - // eslint-disable-next-line no-return-await - data.map(async (v) => await formatTTVSearchResult(v)) - ); - return formatted[0]; - } - - public static async getEpisodes( - slug: string, - season: number - ): Promise { - const data = await Trakt.get( - `/shows/${slug}/seasons/${season}` - ); - - return data.map((e) => ({ - id: e.ids.tmdb, - episode_number: e.number, - title: e.title, - })); - } -} diff --git a/src/backend/metadata/types.ts b/src/backend/metadata/types.ts index 07671e39..e23d9a5b 100644 --- a/src/backend/metadata/types.ts +++ b/src/backend/metadata/types.ts @@ -46,63 +46,36 @@ export interface MWQuery { type: MWMediaType; } -export type TTVContentTypes = "movie" | "show"; +export type TMDBContentTypes = "movie" | "show"; -export type TTVSeasonShort = { +export type TMDBSeasonShort = { title: string; id: number; season_number: number; }; -export type TTVEpisodeShort = { +export type TMDBEpisodeShort = { title: string; id: number; episode_number: number; }; -export type TTVMediaResult = { +export type TMDBMediaResult = { title: string; poster?: string; id: number; original_release_year?: number; - ttv_entity_id: string; - object_type: TTVContentTypes; - seasons?: TTVSeasonShort[]; + object_type: TMDBContentTypes; + seasons?: TMDBSeasonShort[]; }; -export type TTVSeasonMetaResult = { +export type TMDBSeasonMetaResult = { title: string; id: string; season_number: number; - episodes: TTVEpisodeShort[]; + episodes: TMDBEpisodeShort[]; }; -export interface TTVSearchResult { - type: "movie" | "show"; - score: number; - movie?: { - title: string; - year: number; - ids: { - trakt: number; - slug: string; - imdb: string; - tmdb: number; - }; - }; - show?: { - title: string; - year: number; - ids: { - trakt: number; - slug: string; - tvdb: number; - imdb: string; - tmdb: number; - }; - }; -} - export interface DetailedMeta { meta: MWMediaMeta; imdbId?: string; @@ -255,12 +228,9 @@ export interface TMDBMovieData { export type TMDBMediaDetailsPromise = Promise; export interface TMDBMediaStatic { - getMediaDetails( - id: string, - type: MWMediaType.SERIES - ): TMDBMediaDetailsPromise; - getMediaDetails(id: string, type: MWMediaType.MOVIE): TMDBMediaDetailsPromise; - getMediaDetails(id: string, type: MWMediaType): TMDBMediaDetailsPromise; + getMediaDetails(id: string, type: "show"): TMDBMediaDetailsPromise; + getMediaDetails(id: string, type: "movie"): TMDBMediaDetailsPromise; + getMediaDetails(id: string, type: TMDBContentTypes): TMDBMediaDetailsPromise; } export type JWContentTypes = "movie" | "show"; @@ -312,7 +282,7 @@ export type JWSeasonMetaResult = { episodes: JWEpisodeShort[]; }; -export interface TTVEpisodeResult { +export interface TMDBEpisodeResult { season: number; number: number; title: string; @@ -323,3 +293,89 @@ export interface TTVEpisodeResult { tmdb: number; }; } + +export interface TMDBShowResult { + adult: boolean; + backdrop_path: string | null; + genre_ids: number[]; + id: number; + origin_country: string[]; + original_language: string; + original_name: string; + overview: string; + popularity: number; + poster_path: string | null; + first_air_date: string; + name: string; + vote_average: number; + vote_count: number; +} + +export interface TMDBShowResponse { + page: number; + results: TMDBShowResult[]; + total_pages: number; + total_results: number; +} + +export interface TMDBMovieResult { + adult: boolean; + backdrop_path: string | null; + genre_ids: number[]; + id: number; + original_language: string; + original_title: string; + overview: string; + popularity: number; + poster_path: string | null; + release_date: string; + title: string; + video: boolean; + vote_average: number; + vote_count: number; +} + +export interface TMDBMovieResponse { + page: number; + results: TMDBMovieResult[]; + total_pages: number; + total_results: number; +} + +export type TMDBSearchResultsPromise = Promise< + TMDBShowResponse | TMDBMovieResponse +>; + +export interface TMDBSearchResultStatic { + searchMedia(query: string, type: TMDBContentTypes): TMDBSearchResultsPromise; + searchMedia(query: string, type: "movie"): TMDBSearchResultsPromise; + searchMedia(query: string, type: "show"): TMDBSearchResultsPromise; +} + +export interface TMDBEpisode { + air_date: string; + episode_number: number; + id: number; + name: string; + overview: string; + production_code: string; + runtime: number; + season_number: number; + show_id: number; + still_path: string | null; + vote_average: number; + vote_count: number; + crew: any[]; + guest_stars: any[]; +} + +export interface TMDBSeason { + _id: string; + air_date: string; + episodes: TMDBEpisode[]; + name: string; + overview: string; + id: number; + poster_path: string | null; + season_number: number; +} diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index b87654a9..695027a2 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -1,7 +1,7 @@ import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; -import { TTVMediaToId } from "@/backend/metadata/getmeta"; +import { TMDBMediaToId } from "@/backend/metadata/getmeta"; import { MWMediaMeta } from "@/backend/metadata/types"; import { DotList } from "@/components/text/DotList"; @@ -132,7 +132,7 @@ export function MediaCard(props: MediaCardProps) { const canLink = props.linkable && !props.closable; let link = canLink - ? `/media/${encodeURIComponent(TTVMediaToId(props.media))}` + ? `/media/${encodeURIComponent(TMDBMediaToId(props.media))}` : "#"; if (canLink && props.series) link += `/${encodeURIComponent(props.series.seasonId)}/${encodeURIComponent( diff --git a/src/video/components/popouts/EpisodeSelectionPopout.tsx b/src/video/components/popouts/EpisodeSelectionPopout.tsx index 7ea6c5a3..ce45c318 100644 --- a/src/video/components/popouts/EpisodeSelectionPopout.tsx +++ b/src/video/components/popouts/EpisodeSelectionPopout.tsx @@ -2,7 +2,7 @@ import { useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useParams } from "react-router-dom"; -import { decodeTTVId, getMetaFromId } from "@/backend/metadata/getmeta"; +import { decodeTMDBId, getMetaFromId } from "@/backend/metadata/getmeta"; import { MWMediaType, MWSeasonWithEpisodeMeta } from "@/backend/metadata/types"; import { IconPatch } from "@/components/buttons/IconPatch"; import { Icon, Icons } from "@/components/Icon"; @@ -44,7 +44,7 @@ export function EpisodeSelectionPopout() { seasonId: sId, season: undefined, }); - reqSeasonMeta(decodeTTVId(params.media)?.id as string, sId).then((v) => { + reqSeasonMeta(decodeTMDBId(params.media)?.id as string, sId).then((v) => { if (v?.meta.type !== MWMediaType.SERIES) return; setCurrentVisibleSeason({ seasonId: sId, diff --git a/src/views/media/MediaView.tsx b/src/views/media/MediaView.tsx index c0e81455..7ae1c01b 100644 --- a/src/views/media/MediaView.tsx +++ b/src/views/media/MediaView.tsx @@ -6,7 +6,7 @@ import { useHistory, useParams } from "react-router-dom"; import { MWStream } from "@/backend/helpers/streams"; import { DetailedMeta, - decodeTTVId, + decodeTMDBId, getMetaFromId, } from "@/backend/metadata/getmeta"; import { MWMediaType, MWSeasonWithEpisodeMeta } from "@/backend/metadata/types"; @@ -184,7 +184,7 @@ export function MediaView() { const [selected, setSelected] = useState(null); const [exec, loading, error] = useLoading( async (mediaParams: string, seasonId?: string) => { - const data = decodeTTVId(mediaParams); + const data = decodeTMDBId(mediaParams); if (!data) return null; return getMetaFromId(data.type, data.id, seasonId); } From 763de37e9eaa4f6933e631fcd4b06c5703d37a2e Mon Sep 17 00:00:00 2001 From: castdrian Date: Tue, 13 Jun 2023 21:26:58 +0200 Subject: [PATCH 38/45] cleanup --- src/setup/config.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/setup/config.ts b/src/setup/config.ts index c24117bb..f1db01da 100644 --- a/src/setup/config.ts +++ b/src/setup/config.ts @@ -8,7 +8,6 @@ interface Config { TMDB_API_KEY: string; CORS_PROXY_URL: string; NORMAL_ROUTER: boolean; - TRAKT_CLIENT_ID: string; } export interface RuntimeConfig { @@ -19,7 +18,6 @@ export interface RuntimeConfig { TMDB_API_KEY: string; NORMAL_ROUTER: boolean; PROXY_URLS: string[]; - TRAKT_CLIENT_ID: string; } const env: Record = { @@ -30,7 +28,6 @@ const env: Record = { DISCORD_LINK: undefined, CORS_PROXY_URL: import.meta.env.VITE_CORS_PROXY_URL, NORMAL_ROUTER: import.meta.env.VITE_NORMAL_ROUTER, - TRAKT_CLIENT_ID: import.meta.env.VITE_TRAKT_CLIENT_ID, }; const alerts = [] as string[]; @@ -65,6 +62,5 @@ export function conf(): RuntimeConfig { .split(",") .map((v) => v.trim()), NORMAL_ROUTER: getKey("NORMAL_ROUTER", "false") === "true", - TRAKT_CLIENT_ID: getKey("TRAKT_CLIENT_ID"), }; } From 0e9263b6192353b13640da33fbd61e1b990d9aba Mon Sep 17 00:00:00 2001 From: castdrian Date: Wed, 14 Jun 2023 07:48:31 +0200 Subject: [PATCH 39/45] fix movie metadata --- src/backend/metadata/getmeta.ts | 56 ++++++++++++++++++++++----------- src/backend/metadata/search.ts | 2 ++ 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index 777fae42..41e67772 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -43,6 +43,41 @@ export interface DetailedMeta { tmdbId?: string; } +export function fromatTMDBMetaResult( + details: TMDBShowData | TMDBMovieData, + type: MWMediaType +): TMDBMediaResult | undefined { + let tmdbmeta; + if (type === MWMediaType.MOVIE) { + tmdbmeta = { + id: details.id, + title: (details as TMDBMovieData).title, + object_type: mediaTypeToTMDB(type), + poster: (details as TMDBMovieData).poster_path ?? undefined, + original_release_year: Number( + (details as TMDBMovieData).release_date?.split("-")[0] + ), + }; + } + if (type === MWMediaType.SERIES) { + tmdbmeta = { + id: details.id, + title: (details as TMDBShowData).name, + object_type: mediaTypeToTMDB(type), + seasons: (details as TMDBShowData).seasons.map((v) => ({ + id: v.id, + season_number: v.season_number, + title: v.name, + })), + poster: (details as TMDBMovieData).poster_path ?? undefined, + original_release_year: Number( + (details as TMDBShowData).first_air_date?.split("-")[0] + ), + }; + } + return tmdbmeta; +} + export async function getMetaFromId( type: MWMediaType, id: string, @@ -79,25 +114,8 @@ export async function getMetaFromId( } } - const tmdbmeta: TMDBMediaResult = { - id: details.id, - title: - type === MWMediaType.MOVIE - ? (details as TMDBMovieData).title - : (details as TMDBShowData).name, - object_type: mediaTypeToTMDB(type), - seasons: (details as TMDBShowData).seasons.map((v) => ({ - id: v.id, - season_number: v.season_number, - title: v.name, - })), - poster: (details as TMDBMovieData).poster_path ?? undefined, - original_release_year: - type === MWMediaType.MOVIE - ? Number((details as TMDBMovieData).release_date?.split("-")[0]) - : Number((details as TMDBShowData).first_air_date?.split("-")[0]), - }; - + const tmdbmeta = fromatTMDBMetaResult(details, type); + if (!tmdbmeta) return null; const meta = formatTMDBMeta(tmdbmeta, seasonData); if (!meta) return null; diff --git a/src/backend/metadata/search.ts b/src/backend/metadata/search.ts index 31b2c682..7d06ab2c 100644 --- a/src/backend/metadata/search.ts +++ b/src/backend/metadata/search.ts @@ -29,6 +29,8 @@ export async function searchForMedia(query: MWQuery): Promise { }) ); + console.log(results[0]); + cache.set(query, results, 3600); return results; } From 06eb8e6b6d2b40778e7ae93797d919a08fa8ed52 Mon Sep 17 00:00:00 2001 From: castdrian Date: Wed, 14 Jun 2023 07:52:04 +0200 Subject: [PATCH 40/45] cleanup --- src/backend/metadata/search.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/backend/metadata/search.ts b/src/backend/metadata/search.ts index 7d06ab2c..31b2c682 100644 --- a/src/backend/metadata/search.ts +++ b/src/backend/metadata/search.ts @@ -29,8 +29,6 @@ export async function searchForMedia(query: MWQuery): Promise { }) ); - console.log(results[0]); - cache.set(query, results, 3600); return results; } From c9bac3ed68f2688ceabbeaac68bb845443b551b7 Mon Sep 17 00:00:00 2001 From: castdrian Date: Thu, 15 Jun 2023 08:30:05 +0200 Subject: [PATCH 41/45] show poster in bookmarks --- src/backend/metadata/getmeta.ts | 8 ++++++-- src/components/media/MediaCard.tsx | 5 ++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index 41e67772..bba3948e 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -53,7 +53,9 @@ export function fromatTMDBMetaResult( id: details.id, title: (details as TMDBMovieData).title, object_type: mediaTypeToTMDB(type), - poster: (details as TMDBMovieData).poster_path ?? undefined, + poster: + Tmdb.getMediaPoster((details as TMDBMovieData).poster_path) ?? + undefined, original_release_year: Number( (details as TMDBMovieData).release_date?.split("-")[0] ), @@ -69,7 +71,9 @@ export function fromatTMDBMetaResult( season_number: v.season_number, title: v.name, })), - poster: (details as TMDBMovieData).poster_path ?? undefined, + poster: + Tmdb.getMediaPoster((details as TMDBMovieData).poster_path) ?? + undefined, original_release_year: Number( (details as TMDBShowData).first_air_date?.split("-")[0] ), diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index 695027a2..ece6d293 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -2,6 +2,7 @@ import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import { TMDBMediaToId } from "@/backend/metadata/getmeta"; +import { Tmdb } from "@/backend/metadata/tmdb"; import { MWMediaMeta } from "@/backend/metadata/types"; import { DotList } from "@/components/text/DotList"; @@ -55,7 +56,9 @@ function MediaCardContent({ closable ? "" : "group-hover:rounded-lg", ].join(" ")} style={{ - backgroundImage: media.poster ? `url(${media.poster})` : undefined, + backgroundImage: media.poster + ? `url(${Tmdb.getMediaPoster(media.poster)})` + : undefined, }} > {series ? ( From c08a6c7e54b2dff6d2955eb6ee7f561d8379aa42 Mon Sep 17 00:00:00 2001 From: castdrian Date: Thu, 15 Jun 2023 08:30:57 +0200 Subject: [PATCH 42/45] set adult false in query --- src/backend/metadata/tmdb.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend/metadata/tmdb.ts b/src/backend/metadata/tmdb.ts index f01709bb..aed31c1d 100644 --- a/src/backend/metadata/tmdb.ts +++ b/src/backend/metadata/tmdb.ts @@ -120,12 +120,12 @@ export abstract class Tmdb { switch (type) { case "movie": data = await Tmdb.get( - `search/movie?query=${query}&include_adult=true&language=en-US&page=1` + `search/movie?query=${query}&include_adult=false&language=en-US&page=1` ); break; case "show": data = await Tmdb.get( - `search/tv?query=${query}&include_adult=true&language=en-US&page=1` + `search/tv?query=${query}&include_adult=false&language=en-US&page=1` ); break; default: From 4d51de3bd126feaa4585b3041242f73029cf600f Mon Sep 17 00:00:00 2001 From: castdrian Date: Thu, 15 Jun 2023 11:06:24 +0200 Subject: [PATCH 43/45] undo duplicate path --- src/backend/metadata/getmeta.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index bba3948e..41e67772 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -53,9 +53,7 @@ export function fromatTMDBMetaResult( id: details.id, title: (details as TMDBMovieData).title, object_type: mediaTypeToTMDB(type), - poster: - Tmdb.getMediaPoster((details as TMDBMovieData).poster_path) ?? - undefined, + poster: (details as TMDBMovieData).poster_path ?? undefined, original_release_year: Number( (details as TMDBMovieData).release_date?.split("-")[0] ), @@ -71,9 +69,7 @@ export function fromatTMDBMetaResult( season_number: v.season_number, title: v.name, })), - poster: - Tmdb.getMediaPoster((details as TMDBMovieData).poster_path) ?? - undefined, + poster: (details as TMDBMovieData).poster_path ?? undefined, original_release_year: Number( (details as TMDBShowData).first_air_date?.split("-")[0] ), From 0d249a3e27fee182b33980c72837804fa5579a13 Mon Sep 17 00:00:00 2001 From: castdrian Date: Thu, 15 Jun 2023 22:13:19 +0200 Subject: [PATCH 44/45] fix typo 'cause I can't type --- src/backend/metadata/getmeta.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index 41e67772..a5246fcf 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -43,7 +43,7 @@ export interface DetailedMeta { tmdbId?: string; } -export function fromatTMDBMetaResult( +export function formatTMDBMetaResult( details: TMDBShowData | TMDBMovieData, type: MWMediaType ): TMDBMediaResult | undefined { @@ -114,7 +114,7 @@ export async function getMetaFromId( } } - const tmdbmeta = fromatTMDBMetaResult(details, type); + const tmdbmeta = formatTMDBMetaResult(details, type); if (!tmdbmeta) return null; const meta = formatTMDBMeta(tmdbmeta, seasonData); if (!meta) return null; From 205248a376d9347dac3c0f075bf05b3ff280a1f2 Mon Sep 17 00:00:00 2001 From: castdrian Date: Fri, 16 Jun 2023 11:18:32 +0200 Subject: [PATCH 45/45] use external ids endpoint for imdb ids --- src/backend/metadata/getmeta.ts | 6 ++---- src/backend/metadata/tmdb.ts | 25 +++++++++++++++++++++++++ src/backend/metadata/types.ts | 24 ++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index a5246fcf..6868c7e4 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -87,10 +87,8 @@ export async function getMetaFromId( if (!details) return null; - let imdbId; - if (type === MWMediaType.MOVIE) { - imdbId = (details as TMDBMovieData).imdb_id ?? undefined; - } + const externalIds = await Tmdb.getExternalIds(id, mediaTypeToTMDB(type)); + const imdbId = externalIds.imdb_id ?? undefined; let seasonData: TMDBSeasonMetaResult | undefined; diff --git a/src/backend/metadata/tmdb.ts b/src/backend/metadata/tmdb.ts index aed31c1d..e22e86e7 100644 --- a/src/backend/metadata/tmdb.ts +++ b/src/backend/metadata/tmdb.ts @@ -6,15 +6,18 @@ import { MWSeasonMeta, TMDBContentTypes, TMDBEpisodeShort, + TMDBExternalIds, TMDBMediaResult, TMDBMediaStatic, TMDBMovieData, + TMDBMovieExternalIds, TMDBMovieResponse, TMDBMovieResult, TMDBSearchResultStatic, TMDBSeason, TMDBSeasonMetaResult, TMDBShowData, + TMDBShowExternalIds, TMDBShowResponse, TMDBShowResult, } from "./types"; @@ -170,6 +173,28 @@ export abstract class Tmdb { title: e.name, })); } + + public static async getExternalIds( + id: string, + type: TMDBContentTypes + ): Promise { + let data; + + switch (type) { + case "movie": + data = await Tmdb.get( + `/movie/${id}/external_ids` + ); + break; + case "show": + data = await Tmdb.get(`/tv/${id}/external_ids`); + break; + default: + throw new Error("Invalid media type"); + } + + return data; + } } export async function formatTMDBSearchResult( diff --git a/src/backend/metadata/types.ts b/src/backend/metadata/types.ts index e23d9a5b..fa7a7ef0 100644 --- a/src/backend/metadata/types.ts +++ b/src/backend/metadata/types.ts @@ -379,3 +379,27 @@ export interface TMDBSeason { poster_path: string | null; season_number: number; } + +export interface TMDBShowExternalIds { + id: number; + imdb_id: null | string; + freebase_mid: null | string; + freebase_id: null | string; + tvdb_id: number; + tvrage_id: null | string; + wikidata_id: null | string; + facebook_id: null | string; + instagram_id: null | string; + twitter_id: null | string; +} + +export interface TMDBMovieExternalIds { + id: number; + imdb_id: null | string; + wikidata_id: null | string; + facebook_id: null | string; + instagram_id: null | string; + twitter_id: null | string; +} + +export type TMDBExternalIds = TMDBShowExternalIds | TMDBMovieExternalIds;