Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 42 additions & 3 deletions web/src/components/Add/AddDialog.jsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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'
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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 => {
Expand All @@ -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
Expand Down Expand Up @@ -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(() => {
Expand Down
7 changes: 1 addition & 6 deletions web/src/components/Add/RightSideComponent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -237,12 +237,7 @@ export default function RightSideComponent({

<UpdatePosterButton
onClick={() => {
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'
Expand Down
38 changes: 38 additions & 0 deletions web/src/components/Add/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down
12 changes: 11 additions & 1 deletion web/src/components/Search/SearchDialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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) {
Expand Down