diff --git a/cypress/e2e/indexers/tvdb.cy.ts b/cypress/e2e/indexers/tvdb.cy.ts deleted file mode 100644 index d79aa873..00000000 --- a/cypress/e2e/indexers/tvdb.cy.ts +++ /dev/null @@ -1,92 +0,0 @@ -describe('TVDB Integration', () => { - // Constants for routes and selectors - const ROUTES = { - home: '/', - tvdbSettings: '/settings/tvdb', - tomorrowIsOursTvShow: '/tv/72879', - monsterTvShow: '/tv/225634', - }; - - const SELECTORS = { - sidebarToggle: '[data-testid=sidebar-toggle]', - sidebarSettingsMobile: '[data-testid=sidebar-menu-settings-mobile]', - settingsNavDesktop: 'nav[data-testid="settings-nav-desktop"]', - tvdbEnable: 'input[data-testid="tvdb-enable"]', - tvdbSaveButton: '[data-testid=tvbd-save-button]', - heading: '.heading', - season1: 'Season 1', - season2: 'Season 2', - }; - - // Reusable commands - const toggleTVDBSetting = () => { - cy.intercept('/api/v1/settings/tvdb').as('tvdbRequest'); - cy.get(SELECTORS.tvdbSaveButton).click(); - return cy.wait('@tvdbRequest'); - }; - - const verifyTVDBResponse = (response, expectedUseValue) => { - expect(response.statusCode).to.equal(200); - expect(response.body.tvdb).to.equal(expectedUseValue); - }; - - beforeEach(() => { - // Perform login - cy.login(Cypress.env('ADMIN_EMAIL'), Cypress.env('ADMIN_PASSWORD')); - - // Navigate to TVDB settings - cy.visit(ROUTES.home); - cy.get(SELECTORS.sidebarToggle).click(); - cy.get(SELECTORS.sidebarSettingsMobile).click(); - cy.get( - `${SELECTORS.settingsNavDesktop} a[href="${ROUTES.tvdbSettings}"]` - ).click(); - - // Verify heading - cy.get(SELECTORS.heading).should('contain', 'Tvdb'); - - // Configure TVDB settings - cy.get(SELECTORS.tvdbEnable).then(($checkbox) => { - const isChecked = $checkbox.is(':checked'); - - if (!isChecked) { - // If disabled, enable TVDB - cy.wrap($checkbox).click(); - toggleTVDBSetting().then(({ response }) => { - verifyTVDBResponse(response, true); - }); - } else { - // If enabled, disable then re-enable TVDB - cy.wrap($checkbox).click(); - toggleTVDBSetting().then(({ response }) => { - verifyTVDBResponse(response, false); - }); - - cy.wrap($checkbox).click(); - toggleTVDBSetting().then(({ response }) => { - verifyTVDBResponse(response, true); - }); - } - }); - }); - - it('should display "Tomorrow is Ours" show information correctly (1 season on TMDB >1 seasons on TVDB)', () => { - cy.visit(ROUTES.tomorrowIsOursTvShow); - cy.contains(SELECTORS.season2) - .should('be.visible') - .scrollIntoView() - .click(); - }); - - it('Should display "Monster" show information correctly (Not existing on TVDB)', () => { - cy.visit(ROUTES.monsterTvShow); - cy.intercept('/api/v1/tv/225634/season/1').as('season1'); - cy.contains(SELECTORS.season1) - .should('be.visible') - .scrollIntoView() - .click(); - cy.wait('@season1'); - - cy.contains('9 - Hang Men').should('be.visible'); - }); -}); diff --git a/cypress/e2e/providers/tvdb.cy.ts b/cypress/e2e/providers/tvdb.cy.ts new file mode 100644 index 00000000..48d90895 --- /dev/null +++ b/cypress/e2e/providers/tvdb.cy.ts @@ -0,0 +1,127 @@ +describe('TVDB Integration', () => { + // Constants for routes and selectors + const ROUTES = { + home: '/', + metadataSettings: '/settings/metadata', + tomorrowIsOursTvShow: '/tv/72879', + monsterTvShow: '/tv/225634', + }; + + const SELECTORS = { + sidebarToggle: '[data-testid=sidebar-toggle]', + sidebarSettingsMobile: '[data-testid=sidebar-menu-settings-mobile]', + settingsNavDesktop: 'nav[data-testid="settings-nav-desktop"]', + metadataTestButton: 'button[type="button"]:contains("Test")', + metadataSaveButton: '[data-testid="metadata-save-button"]', + tmdbStatus: '[data-testid="tmdb-status"]', + tvdbStatus: '[data-testid="tvdb-status"]', + tvIndexerSelector: '[data-testid="tv-indexer-selector"]', + animeIndexerSelector: '[data-testid="anime-indexer-selector"]', + seasonSelector: '[data-testid="season-selector"]', + season1: 'Season 1', + season2: 'Season 2', + episodeList: '[data-testid="episode-list"]', + episode9: '9 - Hang Men', + }; + + // Reusable commands + const navigateToMetadataSettings = () => { + cy.visit(ROUTES.home); + cy.get(SELECTORS.sidebarToggle).click(); + cy.get(SELECTORS.sidebarSettingsMobile).click(); + cy.get( + `${SELECTORS.settingsNavDesktop} a[href="${ROUTES.metadataSettings}"]` + ).click(); + }; + + const testAndVerifyMetadataConnection = () => { + cy.intercept('POST', '/api/v1/settings/metadatas/test').as( + 'testConnection' + ); + cy.get(SELECTORS.metadataTestButton).click(); + return cy.wait('@testConnection'); + }; + + const saveMetadataSettings = (customBody = null) => { + // Si un corps personnalisé est fourni, utilisez-le pour modifier la requête + if (customBody) { + cy.intercept('PUT', '/api/v1/settings/metadatas', (req) => { + req.body = customBody; + }).as('saveMetadata'); + } else { + // Sinon, juste intercepter sans modifier + cy.intercept('PUT', '/api/v1/settings/metadatas').as('saveMetadata'); + } + + cy.get(SELECTORS.metadataSaveButton).click(); + return cy.wait('@saveMetadata'); + }; + + beforeEach(() => { + // Perform login + cy.login(Cypress.env('ADMIN_EMAIL'), Cypress.env('ADMIN_PASSWORD')); + + // Navigate to Metadata settings + navigateToMetadataSettings(); + + // Verify we're on the correct settings page + cy.contains('h3', 'Metadata').should('be.visible'); + + // Configure TVDB as TV provider and test connection + // Supposons que vous avez ajouté data-testid au div parent du Select + cy.get('[data-testid="tv-indexer-selector"]').click(); + + // Test the connection + testAndVerifyMetadataConnection().then(({ response }) => { + expect(response.statusCode).to.equal(200); + // Check TVDB connection status + cy.get(SELECTORS.tvdbStatus).should('contain', 'Operational'); + }); + + // Save settings + saveMetadataSettings({ + anime: 'tvdb', + tv: 'tvdb', + }).then(({ response }) => { + expect(response.statusCode).to.equal(200); + expect(response.body.tv).to.equal(false); + }); + }); + + it('should display "Tomorrow is Ours" show information with multiple seasons from TVDB', () => { + // Navigate to the TV show + cy.visit(ROUTES.tomorrowIsOursTvShow); + + // Verify that multiple seasons are displayed (TMDB has only 1 season, TVDB has multiple) + //cy.get(SELECTORS.seasonSelector).should('exist'); + cy.intercept('/api/v1/tv/225634/season/1').as('season1'); + // Select Season 2 and verify it loads + cy.contains(SELECTORS.season2) + .should('be.visible') + .scrollIntoView() + .click(); + + // Verify that episodes are displayed for Season 2 + cy.contains('260 - Episode 506').should('be.visible'); + }); + + it('Should display "Monster" show information correctly when not existing on TVDB', () => { + // Navigate to the TV show + cy.visit(ROUTES.monsterTvShow); + + // Intercept season 1 request + cy.intercept('/api/v1/tv/225634/season/1').as('season1'); + + // Select Season 1 + cy.contains(SELECTORS.season1) + .should('be.visible') + .scrollIntoView() + .click(); + + // Wait for the season data to load + cy.wait('@season1'); + + // Verify specific episode exists + cy.contains(SELECTORS.episode9).should('be.visible'); + }); +}); diff --git a/server/api/tvdb/index.ts b/server/api/tvdb/index.ts index 0a4bc0e4..894678de 100644 --- a/server/api/tvdb/index.ts +++ b/server/api/tvdb/index.ts @@ -20,12 +20,14 @@ import logger from '@server/logger'; interface TvdbConfig { baseUrl: string; maxRequestsPerSecond: number; + maxRequests: number; cachePrefix: AvailableCacheIds; } const DEFAULT_CONFIG: TvdbConfig = { baseUrl: 'https://api4.thetvdb.com/v4', maxRequestsPerSecond: 50, + maxRequests: 20, cachePrefix: 'tvdb' as const, }; @@ -52,8 +54,8 @@ class Tvdb extends ExternalAPI implements TvShowIndexer { { nodeCache: cacheManager.getCache(finalConfig.cachePrefix).data, rateLimit: { + maxRequests: finalConfig.maxRequests, maxRPS: finalConfig.maxRequestsPerSecond, - id: finalConfig.cachePrefix, }, } ); @@ -112,14 +114,25 @@ class Tvdb extends ExternalAPI implements TvShowIndexer { } async login(): Promise { + let body: { apiKey: string; pin?: string } = { + apiKey: + process.env.TVDB_API_KEY || 'e4428e99-1c35-4500-9534-e13c1193b428', + }; + + if (this.pin) { + body = { + ...body, + pin: this.pin, + }; + } + const response = await this.post>( '/login', { - apiKey: process.env.TVDB_API_KEY, + ...body, } ); - this.defaultHeaders.Authorization = `Bearer ${response.data.token}`; this.token = response.data.token; return response.data; @@ -250,9 +263,11 @@ class Tvdb extends ExternalAPI implements TvShowIndexer { private async fetchTvdbShowData(tvdbId: number): Promise { const resp = await this.get>( - `/series/${tvdbId}/extended?meta=episodes`, + `/series/${tvdbId}/extended?meta=episodes&short=true`, { - short: 'true', + headers: { + Authorization: `Bearer ${this.token}`, + }, }, Tvdb.DEFAULT_CACHE_TTL ); @@ -265,12 +280,15 @@ class Tvdb extends ExternalAPI implements TvShowIndexer { return []; } - return tvdbData.seasons + const seasons = tvdbData.seasons .filter( (season) => season.number > 0 && season.type && season.type.type === 'official' ) + .sort((a, b) => a.number - b.number) .map((season) => this.createSeasonData(season, tvdbData)); + + return seasons; } private createSeasonData( @@ -307,18 +325,38 @@ class Tvdb extends ExternalAPI implements TvShowIndexer { private async getTvdbSeasonData( tvdbId: number, seasonNumber: number, - tvId: number, - language: string = Tvdb.DEFAULT_LANGUAGE + tvId: number + //language: string = Tvdb.DEFAULT_LANGUAGE ): Promise { const tvdbData = await this.fetchTvdbShowData(tvdbId); if (!tvdbData) { + logger.error(`Failed to fetch TVDB data for ID: ${tvdbId}`); + return this.createEmptySeasonResponse(tvId); + } + + // get season id + const season = tvdbData.seasons.find( + (season) => + season.number === seasonNumber && + season.type.type && + season.type.type === 'official' + ); + + if (!season) { + logger.error( + `Failed to find season ${seasonNumber} for TVDB ID: ${tvdbId}` + ); return this.createEmptySeasonResponse(tvId); } const resp = await this.get>( - `/series/${tvdbId}/episodes/official/${language}`, - {} + `/seasons/${season.id}/extended`, + { + headers: { + Authorization: `Bearer ${this.token}`, + }, + } ); const seasons = resp.data; @@ -342,6 +380,7 @@ class Tvdb extends ExternalAPI implements TvShowIndexer { tvId: number ): TmdbTvEpisodeResult[] { if (!tvdbSeason || !tvdbSeason.episodes) { + logger.error('No episodes found in TVDB season data'); return []; } @@ -355,6 +394,10 @@ class Tvdb extends ExternalAPI implements TvShowIndexer { index: number, tvId: number ): TmdbTvEpisodeResult { + logger.info( + `Creating episode data for episode: ${episode.name} with index: ${index}` + ); + return { id: episode.id, air_date: episode.aired, @@ -364,9 +407,7 @@ class Tvdb extends ExternalAPI implements TvShowIndexer { season_number: episode.seasonNumber, production_code: '', show_id: tvId, - still_path: episode.image - ? 'https://artworks.thetvdb.com' + episode.image - : '', + still_path: episode.image ? episode.image : '', vote_average: 1, vote_count: 1, }; diff --git a/src/components/MetadataSelector/index.tsx b/src/components/MetadataSelector/index.tsx index 27cd3919..fbaece40 100644 --- a/src/components/MetadataSelector/index.tsx +++ b/src/components/MetadataSelector/index.tsx @@ -11,6 +11,7 @@ enum IndexerType { } type IndexerOptionType = { + testId?: string; value: IndexerType; label: string; icon: React.ReactNode; @@ -23,12 +24,14 @@ const messages = defineMessages('components.MetadataSelector', { }); interface MetadataSelectorProps { + testId: string; value: IndexerType; onChange: (value: IndexerType) => void; isDisabled?: boolean; } const MetadataSelector = ({ + testId = 'indexer-selector', value, onChange, isDisabled = false, @@ -37,11 +40,13 @@ const MetadataSelector = ({ const indexerOptions: IndexerOptionType[] = [ { + testId: 'tmdb-option', value: IndexerType.TMDB, label: intl.formatMessage(messages.tmdbLabel), icon: , }, { + testId: 'tvdb-option', value: IndexerType.TVDB, label: intl.formatMessage(messages.tvdbLabel), icon: , @@ -64,26 +69,28 @@ const MetadataSelector = ({ const formatOptionLabel = (option: IndexerOptionType) => (
{option.icon} - {option.label} + {option.label}
); return ( - option.value === value)} + onChange={(selectedOption) => { + if (selectedOption) { + onChange(selectedOption.value); + } + }} + placeholder={intl.formatMessage(messages.selectIndexer)} + styles={customStyles} + formatOptionLabel={formatOptionLabel} + /> + ); }; diff --git a/src/components/Settings/SettingsMetadata.tsx b/src/components/Settings/SettingsMetadata.tsx index c0ca2cc6..102b2059 100644 --- a/src/components/Settings/SettingsMetadata.tsx +++ b/src/components/Settings/SettingsMetadata.tsx @@ -197,16 +197,22 @@ const SettingsMetadata = () => {
- TheMovieDB: - + TheMovieDB: + {getStatusMessage(providerStatus.tmdb)}
- TheTVDB: - + TheTVDB: + {getStatusMessage(providerStatus.tvdb)} @@ -254,6 +260,7 @@ const SettingsMetadata = () => {
setFieldValue('metadata.tv', value)} isDisabled={isSubmitting} @@ -269,6 +276,7 @@ const SettingsMetadata = () => {
setFieldValue('metadata.anime', value)