Skip to content

Commit 105a824

Browse files
authored
feat: add TMDB settings configuration and UI components (#632)
* feat: add TMDB settings configuration and UI components Signed-off-by: Pavel Pikta <devops@pavelpikta.com> * refactor: update TMDB settings translation to remove 'Settings' prefix Signed-off-by: Pavel Pikta <devops@pavelpikta.com> --------- Signed-off-by: Pavel Pikta <devops@pavelpikta.com>
1 parent 2ab4ae5 commit 105a824

File tree

14 files changed

+291
-5
lines changed

14 files changed

+291
-5
lines changed

server/settings/btsets.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ type TorznabConfig struct {
1717
Name string
1818
}
1919

20+
type TMDBConfig struct {
21+
APIKey string // TMDB API Key
22+
APIURL string // Base API URL (default: https://api.themoviedb.org)
23+
ImageURL string // Image URL (default: https://image.tmdb.org)
24+
ImageURLRu string // Image URL for Russian users (default: https://imagetmdb.com)
25+
}
26+
2027
type BTSets struct {
2128
// Cache
2229
CacheSize int64 // in byte, def 64 MB
@@ -45,6 +52,9 @@ type BTSets struct {
4552
EnableTorznabSearch bool
4653
TorznabUrls []TorznabConfig
4754

55+
// TMDB
56+
TMDBSettings TMDBConfig
57+
4858
// BT Config
4959
EnableIPv6 bool
5060
DisableTCP bool
@@ -151,6 +161,13 @@ func SetDefaultConfig() {
151161
sets.ResponsiveMode = true
152162
sets.ShowFSActiveTorr = true
153163
sets.StoreSettingsInJson = true
164+
// Set default TMDB settings
165+
sets.TMDBSettings = TMDBConfig{
166+
APIKey: "",
167+
APIURL: "https://api.themoviedb.org",
168+
ImageURL: "https://image.tmdb.org",
169+
ImageURLRu: "https://imagetmdb.com",
170+
}
154171
BTsets = sets
155172
if !ReadOnly {
156173
buf, err := json.Marshal(BTsets)
@@ -170,6 +187,15 @@ func loadBTSets() {
170187
if BTsets.ReaderReadAHead < 5 {
171188
BTsets.ReaderReadAHead = 5
172189
}
190+
// Set default TMDB settings if missing (for existing configs)
191+
if BTsets.TMDBSettings.APIURL == "" {
192+
BTsets.TMDBSettings = TMDBConfig{
193+
APIKey: "",
194+
APIURL: "https://api.themoviedb.org",
195+
ImageURL: "https://image.tmdb.org",
196+
ImageURLRu: "https://imagetmdb.com",
197+
}
198+
}
173199
return
174200
}
175201
log.TLogln("Error unmarshal btsets", err)

server/web/api/route.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,5 +60,8 @@ func SetupRoute(route gin.IRouter) {
6060
authorized.GET("/storage/settings", GetStorageSettings)
6161
authorized.POST("/storage/settings", UpdateStorageSettings)
6262

63+
// Add TMDB settings endpoint
64+
authorized.GET("/tmdb/settings", tmdbSettings)
65+
6366
authorized.GET("/ffp/:hash/:id", ffp)
6467
}

server/web/api/tmdb.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package api
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/gin-gonic/gin"
7+
8+
sets "server/settings"
9+
)
10+
11+
// tmdbSettings godoc
12+
//
13+
// @Summary Get TMDB settings
14+
// @Description Get TMDB API configuration
15+
//
16+
// @Tags API
17+
//
18+
// @Produce json
19+
// @Success 200 {object} sets.TMDBConfig "TMDB settings"
20+
// @Router /tmdb/settings [get]
21+
func tmdbSettings(c *gin.Context) {
22+
if sets.BTsets == nil {
23+
c.JSON(http.StatusInternalServerError, gin.H{"error": "Settings not initialized"})
24+
return
25+
}
26+
c.JSON(200, sets.BTsets.TMDBSettings)
27+
}

web/src/components/Add/helpers.js

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,69 @@
11
import axios from 'axios'
22
import parseTorrent from 'parse-torrent'
33
import ptt from 'parse-torrent-title'
4+
import { tmdbSettingsHost } from 'utils/Hosts'
45

5-
export const getMoviePosters = (movieName, language = 'en') => {
6-
const url = `${window.location.protocol}//api.themoviedb.org/3/search/multi`
7-
const imgHost = `${window.location.protocol}//${language === 'ru' ? 'imagetmdb.com' : 'image.tmdb.org'}`
6+
// Cache for TMDB settings to avoid repeated API calls
7+
let tmdbSettingsCache = null
8+
9+
// Clear TMDB settings cache - call this when settings are updated
10+
export const clearTMDBCache = () => {
11+
tmdbSettingsCache = null
12+
}
13+
14+
// Fetch TMDB settings from backend
15+
const getTMDBSettings = async () => {
16+
if (tmdbSettingsCache) {
17+
return tmdbSettingsCache
18+
}
19+
20+
try {
21+
const { data } = await axios.get(tmdbSettingsHost())
22+
tmdbSettingsCache = data
23+
return data
24+
} catch (error) {
25+
console.error('Error fetching TMDB settings:', error)
26+
// Return default values if API fails
27+
return {
28+
APIKey: process.env.REACT_APP_TMDB_API_KEY || '',
29+
APIURL: 'https://api.themoviedb.org/3',
30+
ImageURL: 'https://image.tmdb.org',
31+
ImageURLRu: 'https://imagetmdb.com',
32+
}
33+
}
34+
}
35+
36+
export const getMoviePosters = async (movieName, language = 'en') => {
37+
const settings = await getTMDBSettings()
38+
39+
// If no API key is configured, return null
40+
if (!settings.APIKey) {
41+
return null
42+
}
43+
44+
// Build API URL - automatically append /3/search/multi
45+
let apiURL = settings.APIURL.replace(/^https?:\/\//, '').replace(/\/$/, '')
46+
47+
// If URL doesn't already contain the full path, add /3/search/multi
48+
if (!apiURL.includes('/3/search/multi')) {
49+
// Remove any partial paths that might exist
50+
apiURL = apiURL.replace(/\/3.*$/, '').replace(/\/search.*$/, '')
51+
apiURL = `${apiURL}/3/search/multi`
52+
}
53+
54+
const url = `${window.location.protocol}//${apiURL}`
55+
56+
// Build image URL - strip protocol and trailing slash
57+
const imgHost = `${window.location.protocol}//${
58+
language === 'ru'
59+
? settings.ImageURLRu.replace(/^https?:\/\//, '').replace(/\/$/, '')
60+
: settings.ImageURL.replace(/^https?:\/\//, '').replace(/\/$/, '')
61+
}`
862

963
return axios
1064
.get(url, {
1165
params: {
12-
api_key: process.env.REACT_APP_TMDB_API_KEY,
66+
api_key: settings.APIKey,
1367
language,
1468
include_image_language: `${language},null,en`,
1569
query: movieName,

web/src/components/Settings/SettingsDialog.jsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { FormControlLabel, useMediaQuery, useTheme } from '@material-ui/core'
55
import { settingsHost } from 'utils/Hosts'
66
import { useEffect, useState } from 'react'
77
import { useTranslation } from 'react-i18next'
8+
import { clearTMDBCache } from 'components/Add/helpers'
89
import AppBar from '@material-ui/core/AppBar'
910
import Tabs from '@material-ui/core/Tabs'
1011
import Tab from '@material-ui/core/Tab'
@@ -21,6 +22,7 @@ import PrimarySettingsComponent from './PrimarySettingsComponent'
2122
import SecondarySettingsComponent from './SecondarySettingsComponent'
2223
import MobileAppSettings from './MobileAppSettings'
2324
import TorznabSettings from './TorznabSettings'
25+
import TMDBSettings from './TMDBSettings'
2426

2527
export default function SettingsDialog({ handleClose }) {
2628
const { t } = useTranslation()
@@ -52,6 +54,8 @@ export default function SettingsDialog({ handleClose }) {
5254
sets.ReaderReadAHead = cachePercentage
5355
sets.PreloadCache = preloadCachePercentage
5456
axios.post(settingsHost(), { action: 'set', sets })
57+
// Clear TMDB cache so fresh settings are fetched on next poster search
58+
clearTMDBCache()
5559
localStorage.setItem('isVlcUsed', isVlcUsed)
5660
localStorage.setItem('isInfuseUsed', isInfuseUsed)
5761
localStorage.setItem('isIinaUsed', isIinaUsed)
@@ -137,7 +141,9 @@ export default function SettingsDialog({ handleClose }) {
137141

138142
<Tab label={t('Search')} {...a11yProps(2)} />
139143

140-
<Tab label={t('SettingsDialog.Tabs.App')} {...a11yProps(3)} />
144+
<Tab label={t('TMDB.Settings')} {...a11yProps(3)} />
145+
146+
<Tab label={t('SettingsDialog.Tabs.App')} {...a11yProps(4)} />
141147
</Tabs>
142148
</AppBar>
143149

@@ -173,6 +179,10 @@ export default function SettingsDialog({ handleClose }) {
173179
</TabPanel>
174180

175181
<TabPanel value={selectedTab} index={3} dir={direction}>
182+
<TMDBSettings settings={settings} updateSettings={updateSettings} />
183+
</TabPanel>
184+
185+
<TabPanel value={selectedTab} index={4} dir={direction}>
176186
<MobileAppSettings
177187
isVlcUsed={isVlcUsed}
178188
setIsVlcUsed={setIsVlcUsed}
@@ -200,6 +210,8 @@ export default function SettingsDialog({ handleClose }) {
200210
setCachePercentage(defaultSettings.ReaderReadAHead)
201211
setPreloadCachePercentage(defaultSettings.PreloadCache)
202212
updateSettings(defaultSettings)
213+
// Clear TMDB cache when resetting to defaults
214+
clearTMDBCache()
203215
}}
204216
color='secondary'
205217
variant='outlined'
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { useTranslation } from 'react-i18next'
2+
import { FormGroup, FormHelperText, TextField } from '@material-ui/core'
3+
4+
import { SecondarySettingsContent, SettingSectionLabel } from './style'
5+
6+
export default function TMDBSettings({ settings, updateSettings }) {
7+
const { t } = useTranslation()
8+
const { TMDBSettings } = settings || {}
9+
const {
10+
APIKey = '',
11+
APIURL = 'https://api.themoviedb.org/3',
12+
ImageURL = 'https://image.tmdb.org',
13+
ImageURLRu = 'https://imagetmdb.com',
14+
} = TMDBSettings || {}
15+
16+
const handleChange = (field, value) => {
17+
updateSettings({
18+
TMDBSettings: {
19+
...TMDBSettings,
20+
[field]: value,
21+
},
22+
})
23+
}
24+
25+
return (
26+
<SecondarySettingsContent>
27+
<SettingSectionLabel>{t('TMDB.Settings')}</SettingSectionLabel>
28+
<FormGroup>
29+
<TextField
30+
label={t('TMDB.APIKey')}
31+
value={APIKey}
32+
onChange={e => handleChange('APIKey', e.target.value)}
33+
placeholder='Enter your TMDB API key'
34+
variant='outlined'
35+
size='small'
36+
fullWidth
37+
style={{ marginBottom: 15 }}
38+
/>
39+
<FormHelperText margin='none'>{t('TMDB.APIKeyHint')}</FormHelperText>
40+
</FormGroup>
41+
42+
<FormGroup style={{ marginTop: 20 }}>
43+
<TextField
44+
label={t('TMDB.APIURL')}
45+
value={APIURL}
46+
onChange={e => handleChange('APIURL', e.target.value)}
47+
placeholder='https://api.themoviedb.org/3'
48+
variant='outlined'
49+
size='small'
50+
fullWidth
51+
style={{ marginBottom: 10 }}
52+
/>
53+
<FormHelperText margin='none'>{t('TMDB.APIURLHint')}</FormHelperText>
54+
</FormGroup>
55+
56+
<FormGroup style={{ marginTop: 20 }}>
57+
<TextField
58+
label={t('TMDB.ImageURL')}
59+
value={ImageURL}
60+
onChange={e => handleChange('ImageURL', e.target.value)}
61+
placeholder='https://image.tmdb.org'
62+
variant='outlined'
63+
size='small'
64+
fullWidth
65+
style={{ marginBottom: 10 }}
66+
/>
67+
<FormHelperText margin='none'>{t('TMDB.ImageURLHint')}</FormHelperText>
68+
</FormGroup>
69+
70+
<FormGroup style={{ marginTop: 20 }}>
71+
<TextField
72+
label={t('TMDB.ImageURLRu')}
73+
value={ImageURLRu}
74+
onChange={e => handleChange('ImageURLRu', e.target.value)}
75+
placeholder='https://imagetmdb.com'
76+
variant='outlined'
77+
size='small'
78+
fullWidth
79+
style={{ marginBottom: 10 }}
80+
/>
81+
<FormHelperText margin='none'>{t('TMDB.ImageURLRuHint')}</FormHelperText>
82+
</FormGroup>
83+
<br />
84+
</SecondarySettingsContent>
85+
)
86+
}

web/src/locales/bg/translation.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,17 @@
248248
"TorrentAddedSuccessfully": "Торентът е добавен успешно",
249249
"TorznabHostURL": "URL адрес на Torznab сървъра"
250250
},
251+
"TMDB": {
252+
"Settings": "TMDB",
253+
"APIKey": "TMDB API ключ",
254+
"APIKeyHint": "Въведете вашия TMDB API ключ за търсене на филмови постери. Получете го от https://www.themoviedb.org/settings/api",
255+
"APIURL": "TMDB API URL",
256+
"APIURLHint": "Базов URL за TMDB API. API пътищата се добавят автоматично (по подразбиране: https://api.themoviedb.org)",
257+
"ImageURL": "TMDB Изображение URL",
258+
"ImageURLHint": "Базов URL за TMDB изображения. Пътищата на изображенията се добавят автоматично (по подразбиране: https://image.tmdb.org)",
259+
"ImageURLRu": "TMDB Изображение URL (Русия)",
260+
"ImageURLRuHint": "Алтернативен URL за изображения за руски потребители. Пътищата на изображенията се добавят автоматично (по подразбиране: https://imagetmdb.com)"
261+
},
251262
"Tracker": "Тракер",
252263
"TurnOff": "Изключване",
253264
"Uncategorized": "Некатегоризиран",

web/src/locales/en/translation.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,17 @@
248248
"TorrentAddedSuccessfully": "Torrent added successfully",
249249
"TorznabHostURL": "Torznab Host URL"
250250
},
251+
"TMDB": {
252+
"Settings": "TMDB",
253+
"APIKey": "TMDB API Key",
254+
"APIKeyHint": "Enter your TMDB API key to enable movie poster search. Get one at https://www.themoviedb.org/settings/api",
255+
"APIURL": "TMDB API URL",
256+
"APIURLHint": "Base URL for TMDB API. API paths are added automatically (default: https://api.themoviedb.org)",
257+
"ImageURL": "TMDB Image URL",
258+
"ImageURLHint": "Base URL for TMDB images. Image paths are added automatically (default: https://image.tmdb.org)",
259+
"ImageURLRu": "TMDB Image URL (Russia)",
260+
"ImageURLRuHint": "Alternative image URL for Russian users. Image paths are added automatically (default: https://imagetmdb.com)"
261+
},
251262
"Tracker": "Tracker",
252263
"TurnOff": "Turn Off",
253264
"Uncategorized": "Uncategorized",

web/src/locales/fr/translation.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,17 @@
248248
"TorrentAddedSuccessfully": "Torrent ajouté avec succès",
249249
"TorznabHostURL": "URL de l'hôte Torznab"
250250
},
251+
"TMDB": {
252+
"Settings": "TMDB",
253+
"APIKey": "Clé API TMDB",
254+
"APIKeyHint": "Entrez votre clé API TMDB pour activer la recherche d'affiches de films. Obtenez-en une sur https://www.themoviedb.org/settings/api",
255+
"APIURL": "URL de l'API TMDB",
256+
"APIURLHint": "URL de base pour l'API TMDB. Les chemins d'API sont ajoutés automatiquement (par défaut : https://api.themoviedb.org)",
257+
"ImageURL": "URL d'image TMDB",
258+
"ImageURLHint": "URL de base pour les images TMDB. Les chemins d'image sont ajoutés automatiquement (par défaut : https://image.tmdb.org)",
259+
"ImageURLRu": "URL d'image TMDB (Russie)",
260+
"ImageURLRuHint": "URL d'image alternative pour les utilisateurs russes. Les chemins d'image sont ajoutés automatiquement (par défaut : https://imagetmdb.com)"
261+
},
251262
"Tracker": "Tracker",
252263
"TurnOff": "Éteindre",
253264
"Uncategorized": "Non catégorisé",

web/src/locales/ro/translation.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,17 @@
248248
"TorrentAddedSuccessfully": "Torrent adăugat cu succes",
249249
"TorznabHostURL": "URL-ul gazdei Torznab"
250250
},
251+
"TMDB": {
252+
"Settings": "TMDB",
253+
"APIKey": "Cheie API TMDB",
254+
"APIKeyHint": "Introduceți cheia API TMDB pentru a activa căutarea de postere de filme. Obțineți una de la https://www.themoviedb.org/settings/api",
255+
"APIURL": "URL API TMDB",
256+
"APIURLHint": "URL de bază pentru API-ul TMDB. Căile API sunt adăugate automat (implicit: https://api.themoviedb.org)",
257+
"ImageURL": "URL imagine TMDB",
258+
"ImageURLHint": "URL de bază pentru imaginile TMDB. Căile imaginilor sunt adăugate automat (implicit: https://image.tmdb.org)",
259+
"ImageURLRu": "URL imagine TMDB (Rusia)",
260+
"ImageURLRuHint": "URL alternativ de imagine pentru utilizatorii ruși. Căile imaginilor sunt adăugate automat (implicit: https://imagetmdb.com)"
261+
},
251262
"Tracker": "Tracker",
252263
"TurnOff": "Oprire",
253264
"Uncategorized": "Necategorizat",

0 commit comments

Comments
 (0)