diff --git a/web/src/components/Add/AddDialog.jsx b/web/src/components/Add/AddDialog.jsx index 19dc4226d..6115ce329 100644 --- a/web/src/components/Add/AddDialog.jsx +++ b/web/src/components/Add/AddDialog.jsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import Button from '@material-ui/core/Button' import { torrentsHost, torrentUploadHost } from 'utils/Hosts' import axios from 'axios' @@ -11,11 +11,18 @@ import usePreviousState from 'utils/usePreviousState' import { useQuery } from 'react-query' import { getTorrents } from 'utils/Utils' import parseTorrent from 'parse-torrent' +import ptt from 'parse-torrent-title' import { ButtonWrapper } from 'style/DialogStyles' import { StyledDialog, StyledHeader } from 'style/CustomMaterialUiStyles' import useOnStandaloneAppOutsideClick from 'utils/useOnStandaloneAppOutsideClick' -import { checkImageURL, getMoviePosters, checkTorrentSource, parseTorrentTitle } from './helpers' +import { + checkImageURL, + getMoviePosters, + checkTorrentSource, + parseTorrentTitle, + shortenTitleForPosterSearch, +} from './helpers' import { Content } from './style' import RightSideComponent from './RightSideComponent' import LeftSideComponent from './LeftSideComponent' @@ -48,6 +55,7 @@ export default function AddDialog({ const [skipDebounce, setSkipDebounce] = useState(false) const [isCustomTitleEnabled, setIsCustomTitleEnabled] = useState(false) const [currentSourceHash, setCurrentSourceHash] = useState() + const editModePosterSearchedRef = useRef(false) const ref = useOnStandaloneAppOutsideClick(handleClose) @@ -108,6 +116,20 @@ export default function AddDialog({ setPosterUrl('') } + // Edit mode: init original/parsed title from name so poster can be searched + useEffect(() => { + if (!originalHash || (!originalName && !originalTitle)) return + const source = originalName || originalTitle + setOriginalTorrentTitle(source) + try { + const parsed = ptt.parse(source) + setParsedTitle(parsed?.title || '') + } catch (_) { + setParsedTitle('') + } + editModePosterSearchedRef.current = false + }, [originalHash, originalName, originalTitle]) + useEffect(() => { if (originalHash) { checkImageURL(posterUrl).then(correctImage => { @@ -126,8 +148,9 @@ export default function AddDialog({ removePoster() return } + const query = shortenTitleForPosterSearch(String(movieName).trim()) - getMoviePosters(movieName, language).then(urlList => { + getMoviePosters(query || movieName, language).then(urlList => { if (urlList) { setPosterList(urlList) if (!shouldRefreshMainPoster && isUserInteractedWithPoster) return @@ -167,6 +190,22 @@ export default function AddDialog({ updateTitleFromSource() }, [prevTorrentSourceState, selectedFile, torrentSource, updateTitleFromSource]) + // Edit mode: auto-search poster once when we have title and no poster + useEffect(() => { + if ( + !originalHash || + editModePosterSearchedRef.current || + originalPoster || + !(parsedTitle || originalTitle || title) + ) { + return + } + const searchTitle = parsedTitle || title || originalTitle + if (!shortenTitleForPosterSearch(searchTitle)) return + editModePosterSearchedRef.current = true + posterSearch(searchTitle, posterSearchLanguage, { shouldRefreshMainPoster: true }) + }, [originalHash, originalPoster, parsedTitle, originalTitle, title, posterSearchLanguage, posterSearch]) + const prevTitleState = usePreviousState(title) useEffect(() => { diff --git a/web/src/components/Add/RightSideComponent.jsx b/web/src/components/Add/RightSideComponent.jsx index daf21bc79..170a67c74 100644 --- a/web/src/components/Add/RightSideComponent.jsx +++ b/web/src/components/Add/RightSideComponent.jsx @@ -237,12 +237,7 @@ export default function RightSideComponent({ { - let fixedTitle = isCustomTitleEnabled ? title : originalTorrentTitle ? parsedTitle : title - const titleFixedMatch = fixedTitle.replaceAll(/\./g, ' ').match(/^([\w -]+)/) - if (titleFixedMatch?.length && titleFixedMatch[0].length > 0) { - ;[fixedTitle] = titleFixedMatch - } - + const fixedTitle = isCustomTitleEnabled ? title : originalTorrentTitle ? parsedTitle : title posterSearch(fixedTitle, posterSearchLanguage) }} color='primary' diff --git a/web/src/components/Add/helpers.js b/web/src/components/Add/helpers.js index 5c2d8cca6..652c2c73b 100644 --- a/web/src/components/Add/helpers.js +++ b/web/src/components/Add/helpers.js @@ -91,6 +91,44 @@ export const checkTorrentSource = source => source.match(linkRegex) !== null || source.match(torrsRegex) !== null +/** Max length for TMDB/search API query; long torrent names exceed this. */ +const POSTER_SEARCH_MAX_LEN = 50 +/** Max words to use from title for poster search. */ +const POSTER_SEARCH_MAX_WORDS = 4 + +/** + * Shortens a long torrent title for poster search (TMDB). + * Uses part before " [", " (", " / " and limits by words/length so the API gets a valid query. + * @param {string} fullTitle - Raw torrent title + * @param {{ maxWords?: number, maxLen?: number }} opts - Optional limits + * @returns {string} Short title suitable for getMoviePosters() + */ +export const shortenTitleForPosterSearch = (fullTitle, opts = {}) => { + const maxWords = opts.maxWords ?? POSTER_SEARCH_MAX_WORDS + const maxLen = opts.maxLen ?? POSTER_SEARCH_MAX_LEN + if (!fullTitle || typeof fullTitle !== 'string') return '' + const trimmed = fullTitle.trim() + if (!trimmed) return '' + let base = trimmed + for (const sep of [' [', ' (', ' / ']) { + const i = base.indexOf(sep) + if (i > 0) base = base.slice(0, i).trim() + } + try { + const parsed = ptt.parse(base) + if (parsed?.title && parsed.title.length <= maxLen + 15) base = parsed.title + } catch (_) { + // ignore + } + const words = base.split(/\s+/).filter(Boolean) + const byWords = words.slice(0, maxWords).join(' ') + if (byWords.length <= maxLen) return byWords.trim() + const cut = byWords.slice(0, maxLen) + const lastSpace = cut.lastIndexOf(' ') + const result = lastSpace > 0 ? cut.slice(0, lastSpace) : cut + return result.trim() || trimmed.slice(0, maxLen).trim() +} + export const parseTorrentTitle = (parsingSource, callback) => { parseTorrent.remote(parsingSource, (err, { name, files } = {}) => { if (!name || err) return callback({ parsedTitle: null, originalName: null }) diff --git a/web/src/components/Search/SearchDialog.jsx b/web/src/components/Search/SearchDialog.jsx index aa171261d..e6569b699 100644 --- a/web/src/components/Search/SearchDialog.jsx +++ b/web/src/components/Search/SearchDialog.jsx @@ -24,6 +24,7 @@ import { torznabSearchHost, torrentsHost, settingsHost, searchHost } from 'utils import useOnStandaloneAppOutsideClick from 'utils/useOnStandaloneAppOutsideClick' import { StyledDialog, StyledHeader } from 'style/CustomMaterialUiStyles' import { parseSizeToBytes, formatSizeToClassicUnits } from 'utils/Utils' +import { getMoviePosters, shortenTitleForPosterSearch } from 'components/Add/helpers' import { Content } from './style' @@ -97,12 +98,21 @@ export default function SearchDialog({ handleClose }) { setErrorMsg(t('Torznab.NoLinkFound')) return } + let poster = item.Poster + if (!poster && item.Title) { + const query = shortenTitleForPosterSearch(item.Title) + if (query) { + const urlList = await getMoviePosters(query, 'en') + const [firstPosterUrl] = urlList || [] + if (firstPosterUrl) poster = firstPosterUrl + } + } await axios.post(torrentsHost(), { action: 'add', link, title: item.Title, save_to_db: true, - poster: item.Poster, + poster: poster || '', }) setSuccessMsg(t('Torznab.TorrentAddedSuccessfully')) } catch (error) {