Full re-write. Refactored, cache and watchlink.
- Now the api calls to plex are cached. - Search function has been refactored. - Exists in plex has been re-written to use findPlexItemByTitleAndYear if anything is found we return true. findPlexItemByTitleAndYear is then also used for our new endpoint to get direct watch link for plex item. Function parameters are title and year. Title is used when querying plex and year is used with title to match correct plex search result. The direct link includes the machine identifier so a function was added to get system information from plex (read: 'PLEX_URL/' e.g. base path).
This commit is contained in:
@@ -12,19 +12,35 @@ const redisCache = new RedisCache()
|
||||
// TODO? import class definitions to compare types ?
|
||||
// what would typescript do?
|
||||
|
||||
const matchingTitleOrName = (plex, tmdb) => {
|
||||
if (plex['title'] !== undefined && tmdb['title'] !== undefined)
|
||||
return sanitize(plex.title) === sanitize(tmdb.title)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
const matchingYear = (plex, tmdb) => {
|
||||
return plex.year === tmdb.year
|
||||
}
|
||||
|
||||
const sanitize = (string) => string.toLowerCase()
|
||||
|
||||
const matchingTitleAndYear = (plex, tmdb) => {
|
||||
let matchingTitle, matchingYear;
|
||||
|
||||
if (plex['title'] != null && tmdb['title'] != null)
|
||||
matchingTitle = sanitize(plex.title) == sanitize(tmdb.title);
|
||||
else
|
||||
matchingTitle = false;
|
||||
|
||||
if (plex['year'] != null && tmdb['year'] != null)
|
||||
matchingYear = plex.year == tmdb.year;
|
||||
else
|
||||
matchingYear = false;
|
||||
|
||||
return matchingTitle && matchingYear
|
||||
}
|
||||
|
||||
const successfullResponse = (response) => {
|
||||
const { status, statusText } = response
|
||||
|
||||
if (status === 200) {
|
||||
return response.json()
|
||||
} else {
|
||||
throw { message: statusText, status: status }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Plex {
|
||||
constructor(ip, port=32400, cache=null) {
|
||||
this.plexIP = ip
|
||||
@@ -32,65 +48,110 @@ class Plex {
|
||||
|
||||
this.cache = cache || redisCache;
|
||||
this.cacheTags = {
|
||||
machineInfo: 'mi',
|
||||
search: 's'
|
||||
}
|
||||
}
|
||||
|
||||
matchTmdbAndPlexMedia(plex, tmdb) {
|
||||
if (plex === undefined || tmdb === undefined)
|
||||
return false
|
||||
|
||||
let titleMatches;
|
||||
let yearMatches;
|
||||
|
||||
if (plex instanceof Array) {
|
||||
const plexList = plex
|
||||
|
||||
titleMatches = plexList.map(plexItem => matchingTitleOrName(plexItem, tmdb))
|
||||
yearMatches = plexList.map(plexItem => matchingYear(plexItem, tmdb))
|
||||
titleMatches = titleMatches.includes(true)
|
||||
yearMatches = yearMatches.includes(true)
|
||||
} else {
|
||||
titleMatches = matchingTitleOrName(plex, tmdb)
|
||||
yearMatches = matchingYear(plex, tmdb)
|
||||
}
|
||||
|
||||
return titleMatches && yearMatches
|
||||
}
|
||||
|
||||
existsInPlex(tmdbMovie) {
|
||||
return this.search(tmdbMovie.title)
|
||||
.then(plexMovies => plexMovies.some(plex => this.matchTmdbAndPlexMedia(plex, tmdbMovie)))
|
||||
}
|
||||
|
||||
successfullResponse(response) {
|
||||
const { status, statusText } = response
|
||||
|
||||
if (status === 200) {
|
||||
return response.json()
|
||||
} else {
|
||||
throw { message: statusText, status: status }
|
||||
}
|
||||
}
|
||||
|
||||
search(query) {
|
||||
query = encodeURIComponent(query)
|
||||
const url = `http://${this.plexIP}:${this.plexPort}/hubs/search?query=${query}`
|
||||
fetchMachineIdentifier() {
|
||||
const cacheKey = `plex/${this.cacheTags.machineInfo}`
|
||||
const url = `http://${this.plexIP}:${this.plexPort}/`
|
||||
const options = {
|
||||
timeout: 2000,
|
||||
timeout: 20000,
|
||||
headers: { 'Accept': 'application/json' }
|
||||
}
|
||||
|
||||
return fetch(url, options)
|
||||
.then(this.successfullResponse)
|
||||
.then(this.mapResults)
|
||||
return new Promise((resolve, reject) => this.cache.get(cacheKey)
|
||||
.then(machineInfo => resolve(machineInfo['machineIdentifier']))
|
||||
.catch(() => fetch(url, options))
|
||||
.then(response => response.json())
|
||||
.then(machineInfo => this.cache.set(cacheKey, machineInfo['MediaContainer'], 2628000))
|
||||
.then(machineInfo => resolve(machineInfo['machineIdentifier']))
|
||||
.catch(error => {
|
||||
if (error.type === 'request-timeout') {
|
||||
throw { message: 'Plex did not respond', status: 408, success: false }
|
||||
if (error != undefined && error.type === 'request-timeout') {
|
||||
reject({ message: 'Plex did not respond', status: 408, success: false })
|
||||
}
|
||||
|
||||
throw error
|
||||
reject(error)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
matchTmdbAndPlexMedia(plex, tmdb) {
|
||||
let match;
|
||||
|
||||
if (plex == null || tmdb == null)
|
||||
return false
|
||||
|
||||
if (plex instanceof Array) {
|
||||
let possibleMatches = plex.map(plexItem => matchingTitleAndYear(plexItem, tmdb))
|
||||
match = possibleMatches.includes(true)
|
||||
} else {
|
||||
match = matchingTitleAndYear(plex, tmdb)
|
||||
}
|
||||
|
||||
return match
|
||||
}
|
||||
|
||||
existsInPlex(tmdb) {
|
||||
const plexMatch = this.findPlexItemByTitleAndYear(tmdb.title, tmdb.year)
|
||||
return plexMatch ? true : false
|
||||
}
|
||||
|
||||
|
||||
findPlexItemByTitleAndYear(title, year) {
|
||||
const query = { title, year }
|
||||
|
||||
return this.search(query.title)
|
||||
.then(plexSearchResults => {
|
||||
const matchesInPlex = plexSearchResults.map(plex => this.matchTmdbAndPlexMedia(plex, query))
|
||||
|
||||
if (matchesInPlex.includes(true) === false)
|
||||
return false;
|
||||
|
||||
const firstMatchIndex = matchesInPlex.indexOf(true)
|
||||
return plexSearchResults[firstMatchIndex][0]
|
||||
})
|
||||
}
|
||||
|
||||
getDirectLinkByTitleAndYear(title, year) {
|
||||
const machineIdentifierPromise = this.fetchMachineIdentifier()
|
||||
const matchingObjectInPlexPromise = this.findPlexItemByTitleAndYear(title, year)
|
||||
|
||||
return Promise.all([machineIdentifierPromise, matchingObjectInPlexPromise])
|
||||
.then(([machineIdentifier, matchingObjectInPlex]) => {
|
||||
if (matchingObjectInPlex == false || matchingObjectInPlex == null ||
|
||||
matchingObjectInPlex['key'] == null || machineIdentifier == null)
|
||||
return false
|
||||
|
||||
const keyUriComponent = encodeURIComponent(matchingObjectInPlex.key)
|
||||
return `https://app.plex.tv/desktop#!/server/${machineIdentifier}/details?key=${keyUriComponent}`
|
||||
})
|
||||
}
|
||||
|
||||
search(query) {
|
||||
const cacheKey = `plex/${this.cacheTags.search}:${query}`
|
||||
const url = `http://${this.plexIP}:${this.plexPort}/hubs/search?query=${query}`
|
||||
const options = {
|
||||
timeout: 20000,
|
||||
headers: { 'Accept': 'application/json' }
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => this.cache.get(cacheKey)
|
||||
.then(resolve) // if found in cache resolve
|
||||
.catch(() => fetch(url, options)) // else fetch fresh data
|
||||
.then(successfullResponse)
|
||||
.then(this.mapResults)
|
||||
.then(results => this.cache.set(cacheKey, results, 600))
|
||||
.then(resolve)
|
||||
.catch(error => {
|
||||
if (error != undefined && error.type === 'request-timeout') {
|
||||
reject({ message: 'Plex did not respond', status: 408, success: false })
|
||||
}
|
||||
|
||||
reject(error)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
mapResults(response) {
|
||||
@@ -103,10 +164,7 @@ class Plex {
|
||||
.filter(category => category.size > 0)
|
||||
.map(category => {
|
||||
if (category.type === 'movie') {
|
||||
return category.Metadata.map(movie => {
|
||||
const ovie = Movie.convertFromPlexResponse(movie)
|
||||
return ovie.createJsonResponse()
|
||||
})
|
||||
return category.Metadata
|
||||
} else if (category.type === 'show') {
|
||||
return category.Metadata.map(convertPlexToShow)
|
||||
} else if (category.type === 'episode') {
|
||||
@@ -114,7 +172,7 @@ class Plex {
|
||||
}
|
||||
})
|
||||
.filter(result => result !== undefined)
|
||||
.flat()
|
||||
// .flat()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user