├── .gitignore ├── build.bat ├── config.js ├── deploy.sh ├── index.js ├── manifest.js ├── manifest.json ├── package.json ├── server.js ├── sortOpts.json ├── swagger-specs.json ├── tmdb.js ├── trakt-api.js └── vue ├── .env.development ├── .gitignore ├── .vscode └── extensions.json ├── README.md ├── assets ├── Palestine.png └── logo.png ├── dist ├── assets │ ├── index.ac233697.css │ └── index.d78a9e35.js ├── background.png ├── clipboard.svg ├── index.html ├── logo.png └── logoPS.png ├── index.html ├── package.json ├── postcss.config.cjs ├── public ├── background.png ├── clipboard.svg ├── logo.png └── logoPS.png ├── src ├── App.vue ├── components │ └── SearchModal.vue ├── main.js └── style.css ├── tailwind.config.cjs └── vite.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore ALL .log files 2 | *.log 3 | /node_modules 4 | beamup.json 5 | package-lock.json 6 | /old 7 | .env -------------------------------------------------------------------------------- /build.bat: -------------------------------------------------------------------------------- 1 | cd vue 2 | npm run build 3 | cd ../ 4 | echo 'done' -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | var ENV = process.env.NODE_ENV ? 'beamup' : 'local'; 2 | require('dotenv').config(); 3 | const sortOpts = require('./sortOpts.json'); 4 | 5 | function getSortValues(){ 6 | const values = []; 7 | const {SortOptions,SortDirections} = sortOpts; 8 | SortOptions.forEach((sort) => { 9 | SortDirections.forEach((direction) => { 10 | values.push(`${sort.value} ${direction.value}`); 11 | }) 12 | }) 13 | return values; 14 | } 15 | 16 | function getConfig(env = ENV){ 17 | var config = { 18 | host: "https://api.trakt.tv", 19 | 'API_KEY': process.env.API_KEY, 20 | 'client_secret': process.env.client_secret, 21 | 'client_id': process.env.client_id, 22 | 'tmdb': process.env.tmdb 23 | } 24 | 25 | //config.CacheControl = 'max-age=86400, stale-while-revalidate=43200, stale-if-error=86400, public'; 26 | 27 | config.CacheControl = 'max-age=3600, stale-while-revalidate=1800, stale-if-error=3600, public'; 28 | config.lists_array = { 'trakt_trending': "trakt - Trending", 'trakt_popular': "trakt - Popular", 'trakt_watchlist': "trakt - Watchlist", 'trakt_rec': "trakt - Recommended" }; 29 | config.genres = ["action", "adventure", "animation", "anime", "comedy", "crime", "disaster", "documentary", "Donghua", "drama", "eastern", "family", "fan-film", "fantasy", "film-noir", "history", "holiday", "horror", "indie", "music", "musical", "mystery", "none", "road", "romance", "science-fiction", "short", "sports", "sporting-event", "suspense", "thriller", "tv-movie", "war", "western"]; 30 | config.sort_array = getSortValues() // ["added asc", "added desc", "title asc", "title desc", "released asc", "released desc", "runtime asc", "runtime desc", "votes asc", "votes desc", "rating asc", "rating desc", "rank asc", "rank desc"]; 31 | config.count = 100; 32 | switch (env) { 33 | case 'beamup': 34 | config.port = process.env.PORT; 35 | config.local = "https://2ecbbd610840-trakt.baby-beamup.club" 36 | break; 37 | 38 | case 'local': 39 | config.port = 63355; 40 | config.local = "http://127.0.0.1:" + config.port; 41 | break; 42 | } 43 | return config; 44 | } 45 | 46 | 47 | module.exports = getConfig; -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #set -e 4 | 5 | cd vue 6 | npm i 7 | npm run build 8 | cd ../ 9 | #git add --all 10 | #git commit -am "Deploy" 11 | #git push origin main 12 | #git push beamup main 13 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | const cors = require('cors'); 4 | const path = require('path'); 5 | const { getToken, generic_lists, list_catalog, list_cat, listOfLists, getMeta, search, getUserProfile } = require('./trakt-api.js'); 6 | const manifest = require("./manifest.json"); 7 | const config = require('./config.js')(); 8 | 9 | // create monitoring stats routes using swagger-stats 10 | const swStats = require('swagger-stats'); 11 | const apiSpec = require('./swagger-specs.json') 12 | 13 | app.use(swStats.getMiddleware( 14 | { 15 | swaggerSpec: apiSpec, 16 | name: manifest.name, 17 | version: manifest.version, 18 | authentication: true, 19 | onAuthenticate: function (req, username, password) { 20 | // simple check for username and password 21 | return ((username === process.env.USER) 22 | && (password === process.env.PASS)); 23 | } 24 | 25 | } 26 | )); 27 | 28 | 29 | app.set('trust proxy', true) 30 | app.use(cors()); 31 | 32 | app.use('/configure', express.static(path.join(__dirname, 'vue', 'dist'))); 33 | app.use('/assets', express.static(path.join(__dirname, 'vue', 'dist', 'assets'))); 34 | app.use('/public', express.static(path.join(__dirname, 'vue', 'public'))); 35 | 36 | const { lists_array, genres, sort_array, host, count } = config; 37 | 38 | function cacheHeaders(req, res, next) { 39 | if (res.statusCode == 200) { 40 | res.setHeader('Cache-Control', config.CacheControl); 41 | //res.setHeader('content-type', 'text/html'); 42 | } 43 | next(); 44 | } 45 | 46 | app.get('/manifest.json',cacheHeaders, (req, res) => { 47 | 48 | manifest.catalogs = [{ 49 | "type": "trakt", 50 | 51 | "id": "trakt_popular_movies", 52 | 53 | "name": "trakt - Popular movies", 54 | 55 | "extra": [{ "name": "genre", "isRequired": false, "options": genres }, { "name": "skip", "isRequired": false }] 56 | }, { 57 | "type": "trakt", 58 | 59 | "id": "trakt_trending_movies", 60 | 61 | "name": "trakt - Trending movies", 62 | 63 | "extra": [{ "name": "genre", "isRequired": false, "options": genres }, { "name": "skip", "isRequired": false }] 64 | }, { 65 | "type": "trakt", 66 | 67 | "id": "trakt_popular_series", 68 | 69 | "name": "trakt - Popular series", 70 | 71 | "extra": [{ "name": "genre", "isRequired": false, "options": genres }, { "name": "skip", "isRequired": false }] 72 | }, { 73 | "type": "trakt", 74 | 75 | "id": "trakt_trending_series", 76 | 77 | "name": "trakt - Trending series", 78 | 79 | "extra": [{ "name": "genre", "isRequired": false, "options": genres }, { "name": "skip", "isRequired": false }] 80 | }, { 81 | "type": "trakt", 82 | 83 | "id": "trakt_search_movies", 84 | 85 | "name": "trakt - search movies", 86 | 87 | "extra": [{ name: "search", isRequired: true }] 88 | }, { 89 | "type": "trakt", 90 | 91 | "id": "trakt_search_series", 92 | 93 | "name": "trakt - search series", 94 | 95 | "extra": [{ name: "search", isRequired: true }] 96 | }]; 97 | 98 | res.json(manifest); 99 | res.end(); 100 | }); 101 | 102 | app.get('/getUserProfile', (req, res) => { 103 | if(!req.query.access_token) return res.status(401).send('no access_token provided'); 104 | console.log('getUserProfile', req.query.access_token); 105 | getUserProfile(req.query.access_token) 106 | .then(response => res.json(response.data)) 107 | .catch(e => { 108 | if(e.response?.status == 401) return res.status(401).send('invalid access_token'); 109 | else{ 110 | console.error(e); 111 | res.status(400).send() 112 | } 113 | }); 114 | //req.params.query 115 | }); 116 | 117 | app.get('/lists/:query', (req, res) => { 118 | listOfLists(req.params.query, req.query.token).then(data => res.json(data)).catch(e => {res.status(400).send(),console.error(e)}); 119 | }); 120 | 121 | app.get('/:configuration?/',cacheHeaders, (req, res) => { 122 | //console.log('req.query', req.query) 123 | if (req.query?.code || req.query?.refresh_token) { 124 | getToken({ code: req.query.code, refresh_token: req.query.refresh_token }).then(data => { 125 | let { access_token, refresh_token, created_at, expires_in } = data.data; 126 | if (access_token) { 127 | if (req.params.configuration) 128 | return res.redirect(`/${req.params.configuration}/configure/?access_token=${access_token}&refresh_token=${refresh_token}&expires=${created_at + expires_in}`); 129 | else 130 | return res.redirect(`/configure/?access_token=${access_token}&refresh_token=${refresh_token}&expires=${created_at + expires_in}`); 131 | //return res.redirect('/configure/?access_token=' + data.data.access_token); 132 | } 133 | else { 134 | res.send(data);// res.redirect('/configure/?access_token_undefined'); 135 | res.end(); 136 | } 137 | } 138 | ).catch((e) => { 139 | console.error(e); 140 | //res.end(e); 141 | res.redirect('/configure/?error_getting_access_token'); 142 | }) 143 | } else { 144 | return res.redirect('/configure/') 145 | } 146 | }); 147 | 148 | app.get('/:configuration?/configure',cacheHeaders, (req, res) => { 149 | res.sendFile(path.join(__dirname, 'vue', 'dist', 'index.html')); 150 | }); 151 | 152 | app.get('/:configuration?/manifest.json', async (req, res) => { 153 | try { 154 | //console.log(req.params); 155 | const catalog = []; 156 | let newManifest = JSON.parse(JSON.stringify(manifest)); 157 | let parsedConfig = {}; 158 | let configuration = req.params.configuration; 159 | if (configuration) { 160 | if (configuration.startsWith('lists')) throw "unsupported legacy config format" 161 | configuration = Buffer.from(configuration, 'base64').toString(); 162 | try { 163 | parsedConfig = JSON.parse(configuration); 164 | } catch (e) { 165 | throw "config isn't a valid json"; 166 | } 167 | let { lists, ids, access_token, refresh_token, expires } = parsedConfig || {}; 168 | 169 | //console.log("configuration", configuration) 170 | //console.log(lists, ids, access_token); 171 | 172 | if (lists) { 173 | lists.forEach(list => { 174 | let data = genericLists(list, access_token); 175 | if (data.length) 176 | data.forEach(item => catalog.push(item)) 177 | else 178 | catalog.push(data) 179 | }); 180 | } 181 | 182 | if (expires) newManifest.description += `\n token expires on: ${new Date(expires * 1000).toLocaleString()}`; 183 | 184 | if (ids) { 185 | data = await list_cat(ids, access_token) 186 | if (data) newManifest.catalogs = catalog.concat(data); 187 | newManifest.catalogs = newManifest.catalogs.filter(Boolean); 188 | return res.json(newManifest); 189 | } 190 | } else { 191 | newManifest.catalogs = catalog; 192 | newManifest.catalogs = newManifest.catalogs.filter(Boolean); 193 | return res.json(newManifest); 194 | } 195 | } catch (e) { 196 | console.log(e) 197 | res.status(400).send(e); 198 | res.end(); 199 | } 200 | }); 201 | 202 | app.get('/:configuration?/catalog/:type/:id/:extra?.json', async (req, res) => { 203 | try { 204 | let { configuration, type, id, extra } = req.params; 205 | 206 | //console.log('req.params', req.params); 207 | if (type != "trakt") return res.json(updateAddon('catalog')); 208 | 209 | let skip, genre, search_query, parsedConfig = {}; 210 | skip = 0; 211 | if (extra) { 212 | if (extra) params = Object.fromEntries(new URLSearchParams(extra)); 213 | //console.log(params) 214 | if (params) { 215 | if (params.genre) genre = params.genre.split(' '); 216 | if (params.skip) skip = Math.round(params.skip / 100); 217 | if (params.search) search_query = params['search']; 218 | } 219 | } 220 | skip++; 221 | 222 | //console.log(configuration, type, id); 223 | //console.log('extra: genre:', genre, 'skip:', skip); 224 | 225 | if (configuration) { 226 | if (configuration.startsWith('lists')) return res.json(updateAddon('catalog')); 227 | configuration = Buffer.from(configuration, 'base64').toString(); 228 | try { 229 | parsedConfig = JSON.parse(configuration); 230 | } catch (e) { 231 | return res.json(updateAddon('catalog')); 232 | } 233 | } 234 | let { lists, ids, access_token, RPDBkey } = parsedConfig || {}; 235 | //console.log(lists, ids, access_token, RPDBkey); 236 | 237 | let sort, username, trakt_type; 238 | if (id.startsWith("trakt_list:")) { 239 | id = id.replace('trakt_list:', ''); 240 | 241 | [username, list_id, sort] = id.split(':'); 242 | 243 | if (sort) sort = sort.split(','); 244 | if (genre == undefined && id.split(':').length == 4) { 245 | genre = id.split(':')[2].split(','); 246 | } 247 | //console.log('list_id:', list_id, 'username', username, 'sort', sort); 248 | //console.log(id); 249 | metas = await list_catalog({ id: list_id, username, access_token, genre, sort, skip, RPDBkey }) 250 | if (metas) metas = metas.filter(Boolean); 251 | res.json({ metas: metas }); 252 | 253 | } else if (id.startsWith("trakt")) { 254 | list_id = id.split('_')[1]; 255 | type = id.split('_')[2]; 256 | 257 | if (list_id === 'watchlist') { 258 | let regex = new RegExp(/^(movies|series)$/); 259 | type = regex.test(id.split('_')[2]) ? id.split('_')[2] : null; 260 | if (type) { 261 | trakt_type = type == "movies" ? "movies" : type == "series" ? "shows" : null; 262 | sort = id.split('_')[3]; 263 | 264 | } else { 265 | sort = id.split('_')[2]; 266 | } 267 | //console.log('id',id,'sort',sort); 268 | if (!genre && sort) { 269 | genre = sort.split(','); 270 | } 271 | } else { 272 | if (type == "movies") { 273 | trakt_type = "movie"; 274 | } else if (type == "series") { 275 | trakt_type = "show"; 276 | } 277 | } 278 | 279 | //console.log("list_id", list_id); 280 | const data = { trakt_type: trakt_type, type: type, access_token: access_token, genre: genre, skip: skip, RPDBkey } 281 | 282 | if (list_id && generic_lists.hasOwnProperty(list_id)) { 283 | metas = await generic_lists[list_id](data); 284 | if (metas) metas = metas.filter(Boolean); 285 | res.json({ metas: metas }); 286 | } else if (list_id && list_id == "search" && search_query) { 287 | metas = await search(trakt_type, search_query, RPDBkey); 288 | if (metas) metas = metas.filter(Boolean); 289 | res.json({ metas: metas }); 290 | } 291 | } 292 | res.end(); 293 | } catch (e) { 294 | console.log(e) 295 | res.status(400).send(e); 296 | res.end(); 297 | } 298 | }) 299 | 300 | app.get('/:configuration?/meta/:type/:id/:extra?.json', async (req, res) => { 301 | 302 | let { configuration, type, id, extra } = req.params; 303 | 304 | //console.log('req.params', req.params); 305 | if (id.startsWith("trakt:")) { 306 | id = id.replace('trakt:', ''); 307 | meta = await getMeta(type, id) 308 | if (meta) res.json({ meta: meta }); 309 | res.end(); 310 | 311 | } else { 312 | res.end(); 313 | } 314 | }) 315 | 316 | function genericLists(list, access_token) { 317 | const [id, secondPart, thirdPart] = list.split(':'); 318 | const separated = secondPart == "separated" ? true : false; 319 | const sort = secondPart == "separated" ? thirdPart : secondPart; 320 | 321 | if ((id == 'trakt_trending' || id == 'trakt_popular') || (access_token && access_token.length > 0 && id == 'trakt_rec')) { 322 | return [{ 323 | "type": 'trakt', 324 | 325 | "id": sort ? `${id}_movies_${sort}` : `${id}_movies`, 326 | 327 | "name": lists_array[id] + " movies", 328 | 329 | "extra": [{ "name": "genre", "isRequired": false, "options": genres }, { "name": "skip", "isRequired": false }] 330 | }, { 331 | "type": "trakt", 332 | 333 | "id": sort ? `${id}_series_${sort}` : `${id}_series`, 334 | 335 | "name": lists_array[id] + " series", 336 | 337 | "extra": [{ "name": "genre", "isRequired": false, "options": genres }, { "name": "skip", "isRequired": false }] 338 | }]; 339 | } else if (access_token && access_token.length > 0 && id == 'trakt_watchlist') { 340 | if (separated) { 341 | return [{ 342 | "type": 'trakt', 343 | 344 | "id": sort ? `${id}_movies_${sort}` : `${id}_movies`, 345 | 346 | "name": lists_array[id] + " movies", 347 | 348 | "extra": [{ "name": "genre", "isRequired": false, "options": sort_array }, { "name": "skip", "isRequired": false }] 349 | }, { 350 | "type": 'trakt', 351 | 352 | "id": sort ? `${id}_series_${sort}` : `${id}_series`, 353 | 354 | "name": lists_array[id] + " series", 355 | 356 | "extra": [{ "name": "genre", "isRequired": false, "options": sort_array }, { "name": "skip", "isRequired": false }] 357 | }] 358 | } 359 | else { 360 | return { 361 | "type": 'trakt', 362 | 363 | "id": sort ? `${id}_${sort}` : `${id}`, 364 | 365 | "name": lists_array[id], 366 | 367 | "extra": [{ "name": "genre", "isRequired": false, "options": sort_array }, { "name": "skip", "isRequired": false }] 368 | }; 369 | } 370 | } else if (id == 'trakt_search') { 371 | return [{ 372 | "type": "trakt", 373 | 374 | "id": "trakt_search_movies", 375 | 376 | "name": "trakt - search movies", 377 | 378 | "extra": [{ name: "search", isRequired: true }] 379 | }, { 380 | "type": "trakt", 381 | 382 | "id": "trakt_search_series", 383 | 384 | "name": "trakt - search series", 385 | 386 | "extra": [{ name: "search", isRequired: true }] 387 | }] 388 | } 389 | } 390 | 391 | function updateAddon(resource){ 392 | if(resource == 'catalog'){ 393 | return [ 394 | { 395 | id: 'trakt_updateAddon', 396 | name: 'ERROR: update addon', 397 | type: 'movie', 398 | poster: 'https://trakt.tv/assets/placeholders/thumb/poster-2561df5a41a5cb55c1d4a6f02d6532cf327f175bda97f4f813c18dea3435430c.png', 399 | description: "you're using an old version of this addon, please update it" 400 | } 401 | ] 402 | } 403 | else if(resource == "meta"){ 404 | return { 405 | id: 'trakt_updateAddon', 406 | name: 'ERROR: update addon', 407 | type: 'movie', 408 | poster: 'https://trakt.tv/assets/placeholders/thumb/poster-2561df5a41a5cb55c1d4a6f02d6532cf327f175bda97f4f813c18dea3435430c.png', 409 | description: "you're using an old version of this addon, please update it" 410 | } 411 | } 412 | } 413 | 414 | 415 | 416 | module.exports = app -------------------------------------------------------------------------------- /manifest.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const config = require('./config.js')('beamup'); 3 | 4 | let manifest = { 5 | "id": "community.trakt-tv", 6 | "version": "0.2.7", 7 | "name": "Trakt Tv", 8 | "description": "Addon for getting Trakt's public and user lists, recommendations and watch list.", 9 | }; 10 | 11 | manifest = { ...manifest, 12 | "logo": `${config.local}/public/logoPS.png?ver=${manifest.version}`, 13 | "background": `${config.local}/public/background.png?ver=${manifest.version}`, 14 | "catalogs": [], 15 | "resources": [{ "name": "meta", "types": [ "series","movie" ], "idPrefixes": [ "trakt:" ] }], 16 | "types": [], 17 | "idPrefixes": [ "trakt" ], 18 | "behaviorHints": { 19 | "configurable": true, 20 | "configurationRequired": false 21 | } 22 | } 23 | 24 | fs.writeFileSync('./manifest.json', JSON.stringify(manifest)); 25 | 26 | //module.exports = manifest; -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | {"id":"community.trakt-tv","version":"0.2.7","name":"Trakt Tv","description":"Addon for getting Trakt's public and user lists, recommendations and watch list.","logo":"https://2ecbbd610840-trakt.baby-beamup.club/public/logoPS.png?ver=0.2.7","background":"https://2ecbbd610840-trakt.baby-beamup.club/public/background.png?ver=0.2.7","catalogs":[],"resources":[{"name":"meta","types":["series","movie"],"idPrefixes":["trakt:"]}],"types":[],"idPrefixes":["trakt"],"behaviorHints":{"configurable":true,"configurationRequired":false}} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "community.trakt-tv", 3 | "version": "0.2.7", 4 | "description": "Addon for getting Trakt's public and user lists, recommendations and watch list.", 5 | "main": "server.js", 6 | "scripts": { 7 | "preBuild": "node manifest.js", 8 | "Build": "cd vue && npm i && npm run build", 9 | "prestart": "node manifest.js", 10 | "start": "node server.js", 11 | "dev": "nodemon server.js" 12 | }, 13 | "dependencies": { 14 | "axios": "^1.3.2", 15 | "cors": "^2.8.5", 16 | "dotenv": "^16.0.2", 17 | "express": "^4.16.4", 18 | "node-cache": "^5.1.2", 19 | "prom-client": "^12.0.0", 20 | "stremio-addon-sdk": "latest", 21 | "swagger-stats": "^0.99.4", 22 | "underscore": "^1.13.4" 23 | }, 24 | "devDependencies": { 25 | "nodemon": "^2.0.19" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | // require serverless version 2 | const app = require('./index.js') 3 | const { publishToCentral } = require("stremio-addon-sdk") 4 | const config = require('./config.js')(); 5 | 6 | 7 | // create local server 8 | app.listen((config.port), function () { 9 | console.log(`Addon active on port ${config.port}`); 10 | console.log(`HTTP addon accessible at: ${config.local}/configure`); 11 | }); 12 | 13 | if(process.env.NODE_ENV){ 14 | publishToCentral(`${config.local}/manifest.json`).catch(e=>console.error(e)) 15 | } -------------------------------------------------------------------------------- /sortOpts.json: -------------------------------------------------------------------------------- 1 | { 2 | "SortOptions": [{ "name": "Added Date", "value": "added" }, { "name": "Title", "value": "title" }, { "name": "Release Date", "value": "released" }, { "name": "Runtime", "value": "runtime" }, { "name": "Trakt votes", "value": "votes" }, { "name": "Trakt Percentage", "value": "rating" }, { "name": "Rank", "value": "rank" }, { "name": "Popularity", "value": "popularity" }, { "name": "Random", "value": "random" }], 3 | "SortDirections": [{ "name": "Ascendant", "value": "asc" }, { "name": "Descendant", "value": "desc" }] 4 | } -------------------------------------------------------------------------------- /swagger-specs.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "title": "Trakt Tv", 5 | "version": "0.2.4" 6 | }, 7 | "paths": { 8 | "/": { 9 | "get": { 10 | "summary": "Redirects to the configure route", 11 | "responses": { 12 | "302": { 13 | "description": "Found" 14 | } 15 | } 16 | } 17 | }, 18 | "/public/{file?}": { 19 | "get": { 20 | "summary": "public files", 21 | "parameters": [ 22 | { 23 | "name": "file", 24 | "in": "path", 25 | "description": "requested file", 26 | "required": false, 27 | "schema": { 28 | "type": "string" 29 | } 30 | } 31 | ], 32 | "responses": { 33 | "200": { 34 | "description": "OK" 35 | }, 36 | "400": { 37 | "description": "Bad Request" 38 | } 39 | } 40 | } 41 | }, 42 | "/assets/{file?}": { 43 | "get": { 44 | "summary": "assets files", 45 | "parameters": [ 46 | { 47 | "name": "file", 48 | "in": "path", 49 | "description": "requested file", 50 | "required": false, 51 | "schema": { 52 | "type": "string" 53 | } 54 | } 55 | ], 56 | "responses": { 57 | "200": { 58 | "description": "OK" 59 | }, 60 | "400": { 61 | "description": "Bad Request" 62 | } 63 | } 64 | } 65 | }, 66 | "/{configuration?}/configure": { 67 | "get": { 68 | "summary": "Serves the configuration page", 69 | "parameters": [ 70 | { 71 | "name": "configuration", 72 | "in": "path", 73 | "description": "Configuration parameter", 74 | "required": false, 75 | "schema": { 76 | "type": "string" 77 | } 78 | }, 79 | { 80 | "name": "access_token", 81 | "in": "query", 82 | "required": false, 83 | "schema": { 84 | "type": "string" 85 | } 86 | }, 87 | { 88 | "name": "refresh_token", 89 | "in": "query", 90 | "required": false, 91 | "schema": { 92 | "type": "string" 93 | } 94 | }, 95 | { 96 | "name": "expires", 97 | "in": "query", 98 | "required": false, 99 | "schema": { 100 | "type": "string" 101 | } 102 | } 103 | ], 104 | "responses": { 105 | "200": { 106 | "description": "OK" 107 | }, 108 | "400": { 109 | "description": "Bad Request" 110 | } 111 | } 112 | } 113 | }, 114 | "/lists/{query}": { 115 | "get": { 116 | "summary": "Retrieves a list of lists", 117 | "parameters": [ 118 | { 119 | "name": "query", 120 | "in": "path", 121 | "description": "Query parameter", 122 | "required": true, 123 | "schema": { 124 | "type": "string" 125 | } 126 | } 127 | ], 128 | "responses": { 129 | "200": { 130 | "description": "OK" 131 | } 132 | } 133 | } 134 | }, 135 | "/{configuration?}/manifest.json": { 136 | "get": { 137 | "summary": "Serves the manifest JSON based on the configuration", 138 | "parameters": [ 139 | { 140 | "name": "configuration", 141 | "in": "path", 142 | "description": "Configuration parameter", 143 | "required": false, 144 | "schema": { 145 | "type": "string" 146 | } 147 | } 148 | ], 149 | "responses": { 150 | "200": { 151 | "description": "OK" 152 | }, 153 | "400": { 154 | "description": "Bad Request" 155 | } 156 | } 157 | } 158 | }, 159 | "/{configuration?}/catalog/{type}/{id}/{extra?.json}": { 160 | "get": { 161 | "summary": "Retrieves catalog data", 162 | "parameters": [ 163 | { 164 | "name": "configuration", 165 | "in": "path", 166 | "description": "Configuration parameter", 167 | "required": false, 168 | "schema": { 169 | "type": "string" 170 | } 171 | }, 172 | { 173 | "name": "type", 174 | "in": "path", 175 | "description": "Type parameter", 176 | "required": true, 177 | "schema": { 178 | "type": "string" 179 | } 180 | }, 181 | { 182 | "name": "id", 183 | "in": "path", 184 | "description": "ID parameter", 185 | "required": true, 186 | "schema": { 187 | "type": "string" 188 | } 189 | }, 190 | { 191 | "name": "extra", 192 | "in": "path", 193 | "description": "Extra parameter", 194 | "required": false, 195 | "schema": { 196 | "type": "string" 197 | } 198 | } 199 | ], 200 | "responses": { 201 | "200": { 202 | "description": "OK" 203 | }, 204 | "404": { 205 | "description": "Not Found" 206 | }, 207 | "400": { 208 | "description": "Bad Request" 209 | } 210 | } 211 | } 212 | }, 213 | "/{configuration?}/meta/{type}/{id}/{extra?.json}": { 214 | "get": { 215 | "summary": "Retrieves meta data", 216 | "parameters": [ 217 | { 218 | "name": "configuration", 219 | "in": "path", 220 | "description": "Configuration parameter", 221 | "required": false, 222 | "schema": { 223 | "type": "string" 224 | } 225 | }, 226 | { 227 | "name": "type", 228 | "in": "path", 229 | "description": "Type parameter", 230 | "required": true, 231 | "schema": { 232 | "type": "string" 233 | } 234 | }, 235 | { 236 | "name": "id", 237 | "in": "path", 238 | "description": "ID parameter", 239 | "required": true, 240 | "schema": { 241 | "type": "string" 242 | } 243 | }, 244 | { 245 | "name": "extra", 246 | "in": "path", 247 | "description": "Extra parameter", 248 | "required": false, 249 | "schema": { 250 | "type": "string" 251 | } 252 | } 253 | ], 254 | "responses": { 255 | "200": { 256 | "description": "OK" 257 | }, 258 | "404": { 259 | "description": "Not Found" 260 | }, 261 | "400": { 262 | "description": "Bad Request" 263 | } 264 | } 265 | } 266 | } 267 | } 268 | } -------------------------------------------------------------------------------- /tmdb.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios').default; 2 | const config = require('./config')(); 3 | const NodeCache = require("node-cache"); 4 | const Cache = new NodeCache({ stdTTL: 3600, checkperiod: 600 }); 5 | 6 | const BaseURL = 'https://api.themoviedb.org/3'; 7 | const imagePath = 'https://image.tmdb.org/t/p/original/'; 8 | 9 | 10 | async function request(url, header) { 11 | 12 | return await axios 13 | .get(url, header, { timeout: 5000 }) 14 | .then(res => { 15 | return res; 16 | }) 17 | .catch(error => { 18 | if (error.response) { 19 | console.error('error on tmdb.js request:', error.response.status, error.response.statusText, error.config.url); 20 | } else { 21 | console.error(error); 22 | } 23 | }); 24 | 25 | } 26 | async function getMeta(type, id) { 27 | try { 28 | console.log('getMeta', type, id) 29 | const Cached = Cache.get(id) 30 | if (Cached) return Cached 31 | let data; 32 | let meta = {}; 33 | 34 | if (typeof id == 'string' && id.startsWith('tt')) { 35 | if (type == "movie") { 36 | const url = `${BaseURL}/movie/${id}?api_key=${config.tmdb}` 37 | const res = await request(url); 38 | data = res.data 39 | } else if (type == "series") { 40 | const url = `${BaseURL}/find/${id}?api_key=${config.tmdb}&external_source=imdb_id` 41 | const res = await request(url); 42 | data = res.data.tv_results[0]; 43 | } 44 | } 45 | else { 46 | type = type == 'series' ? 'tv' : 'movie'; 47 | const url = `${BaseURL}/${type}/${id}?api_key=${config.tmdb}` 48 | const res = await request(url); 49 | data = res.data; 50 | } 51 | if (!data) throw "error getting data" 52 | if (data.backdrop_path) meta.background = imagePath + data.backdrop_path; 53 | if (data.poster_path) meta.poster = imagePath + data.poster_path; 54 | Cache.set(id, meta); 55 | return meta 56 | } catch (e) { 57 | console.error(e) 58 | } 59 | } 60 | //getMeta("movie", 786836).then(meta => (console.log(meta))) 61 | 62 | //getMeta("series", 'tt0903747').then(meta => (console.log(meta))) 63 | 64 | module.exports = getMeta; -------------------------------------------------------------------------------- /trakt-api.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios').default; 2 | const config = require('./config.js')(); 3 | const tmdbMeta = require('./tmdb.js'); 4 | const _ = require('underscore'); 5 | 6 | const NodeCache = require('node-cache'); 7 | const Cache = new NodeCache({ stdTTL: 3600, checkperiod: 600 }); 8 | 9 | const { sort_array, host, count, local: myurl } = config; 10 | 11 | client = axios.create({ 12 | baseURL: host, 13 | headers: { 14 | 'Content-type': 'application/json', 15 | 'trakt-api-key': config.API_KEY, 16 | 'trakt-api-version': 2, 17 | 'Accept-Encoding': '*' 18 | }, 19 | timeout: 50000, 20 | }); 21 | 22 | async function request(url = String, header = {}) { 23 | //console.log(header) 24 | 25 | return await client({ 26 | method: 'get', 27 | url: url, 28 | headers: header.headers 29 | }).then(res => { 30 | return res; 31 | }) 32 | .catch(error => { 33 | 34 | //console.error(error); 35 | if (error.response) { 36 | console.error('error on trakt-api.js request:', error.response.status, error.response.statusText, error.config.url); 37 | } else { 38 | console.error(error); 39 | } 40 | }); 41 | 42 | } 43 | 44 | async function popular(user_data = {}) { 45 | try { 46 | 47 | //console.log('user_data',user_data); 48 | const { trakt_type, type, access_token, genre, skip, RPDBkey } = user_data; 49 | 50 | let url = `/${trakt_type}s/popular/?extended=full`; 51 | 52 | if (!url) throw 'error getting url'; 53 | if (skip) url += `&page=${skip}`; 54 | if (count) url += `&limit=${count}`; 55 | if (genre) url += `&genres=${genre}`; 56 | 57 | const data = await request(url); 58 | 59 | if (!data || !data.data) throw 'error getting data (recommended list)'; 60 | 61 | const items = await ConvertToStremio(NormalizeLists(data.data, trakt_type), RPDBkey); 62 | return items; 63 | } catch (e) { 64 | console.error(e); 65 | } 66 | } 67 | 68 | async function trending(user_data = {}) { 69 | try { 70 | const { trakt_type, type, access_token, genre, skip, RPDBkey } = user_data; 71 | 72 | let url = `/${trakt_type}s/trending/?extended=full`; 73 | 74 | if (!url) throw 'error getting url'; 75 | if (skip) url += `&page=${skip}`; 76 | if (count) url += `&limit=${count}`; 77 | 78 | if (genre) url += `&genres=${genre}`; 79 | 80 | const data = await request(url); 81 | 82 | if (!data || !data.data) throw 'error getting data (recommended list)'; 83 | 84 | const items = await ConvertToStremio(NormalizeLists(data.data, trakt_type), RPDBkey); 85 | 86 | return items; 87 | } 88 | catch (e) { 89 | console.error(e) 90 | } 91 | } 92 | 93 | async function watchlist(user_data = {}) { 94 | try { 95 | //console.log(user_data); 96 | const { trakt_type, type, access_token, genre, skip, RPDBkey } = user_data; 97 | 98 | if (!access_token) throw 'access_token is required' 99 | const header = { 100 | headers: { 101 | 'Authorization': `Bearer ${access_token}` 102 | } 103 | }; 104 | let url = `/sync/watchlist`; 105 | if(trakt_type) url += `/${trakt_type}`; 106 | 107 | url += '/?extended=full'; 108 | const data = await request(url, header); 109 | 110 | if (!data || !data.data) throw 'error getting data (recommended list)'; 111 | 112 | let items = NormalizeLists(data.data); 113 | 114 | if (genre && genre.length) items = SortList(items, genre); 115 | 116 | if (items && items.length > 100) { 117 | if ((skip * 100) < items.length) items = items.slice((skip - 1) * 100, skip * 100); 118 | else items = items.slice(items.length - 100, items.length); 119 | } 120 | 121 | if (items) return await ConvertToStremio(items, RPDBkey) 122 | 123 | return await ConvertToStremio(items, RPDBkey); 124 | } catch (e) { 125 | console.error(e) 126 | } 127 | }; 128 | 129 | async function recomendations(user_data = {}) { 130 | try { 131 | const { trakt_type, type, access_token, genre, skip, RPDBkey } = user_data; 132 | 133 | 134 | if (!access_token) throw 'access_token is required' 135 | 136 | const header = { 137 | headers: { 138 | 'Authorization': `Bearer ${access_token}` 139 | } 140 | }; 141 | 142 | let url = `/recommendations/${trakt_type}s?limit=${count}&extended=full`; 143 | if (skip !== undefined) url += `&page=${skip}`; 144 | if (genre !== undefined) url += `&genres=${genre}`; 145 | 146 | //console.log('access_token', access_token) 147 | 148 | const data = await request(url, header); 149 | if (!data || !data.data) throw 'error getting data (recommended list)'; 150 | 151 | const items = await ConvertToStremio(NormalizeLists(data.data, trakt_type), RPDBkey); 152 | return items; 153 | 154 | } catch (e) { 155 | console.error(e); 156 | } 157 | } 158 | 159 | async function search(trakt_type = String, query = String, RPDBkey = {}) { 160 | try { 161 | 162 | let url = `/search/${trakt_type}?query=${encodeURIComponent(query)}&extended=full`; 163 | 164 | const data = await request(url); 165 | if (!data || !data.data) throw 'error getting data (search)'; 166 | const items = await ConvertToStremio(NormalizeLists(data.data, trakt_type), RPDBkey); 167 | 168 | return items; 169 | 170 | } catch (e) { 171 | console.error(e); 172 | } 173 | } 174 | 175 | async function list_catalog(list = {}) { 176 | try { 177 | 178 | //const { trakt_type, type, access_token, genre, skip, RPDBkey } = user_data; 179 | 180 | let { id, username, access_token, genre, sort, skip, RPDBkey } = list; 181 | const cached_id = username ? `${id}:${username}` : id; 182 | genre = genre ? genre : sort; 183 | 184 | const Cached = Cache.get(cached_id); 185 | 186 | if (Cached) return await ConvertToStremio(Cached, RPDBkey); 187 | else { 188 | let url, header; 189 | if (username) { 190 | url = `/users/${username}/lists/${id}/items?extended=full`; 191 | if (access_token) { 192 | header = { 193 | headers: { 194 | 'Authorization': `Bearer ${access_token}` 195 | } 196 | } 197 | } 198 | } 199 | else url = `/lists/${id}/items/?extended=full`; 200 | //console.log(url) 201 | 202 | const data = await request(url, header); 203 | if (!data || !data.data) throw 'error getting data (recommended list)'; 204 | 205 | 206 | 207 | 208 | let items = NormalizeLists(data.data); 209 | 210 | if (genre && genre.length) items = SortList(items, genre); 211 | 212 | if (items && items.length > 100) { 213 | if ((skip * 100) < items.length) items = items.slice((skip - 1) * 100, skip * 100); 214 | else items = items.slice(items.length - 100, items.length); 215 | } 216 | 217 | if (items) return await ConvertToStremio(items, RPDBkey) 218 | /* 219 | const NormalizedItems = NormalizeLists(data.data); 220 | Cache.set(cached_id, NormalizedItems); 221 | const items = SortList(await ConvertToStremio(NormalizedItems,RPDBkey),genre); 222 | return items;*/ 223 | } 224 | } catch (e) { 225 | console.error(e); 226 | } 227 | 228 | } 229 | 230 | function SortList(items = [], sort = []) { 231 | //console.log('sorting', sort) 232 | if (!sort || !sort.length || sort[0] == ',') return items; 233 | let [sort_by, sort_how] = sort; 234 | //console.log(items[0]); 235 | items.forEach(item=>{ 236 | //if(!item.votes||!item.rating) console.log(item); 237 | //console.log(item) 238 | }) 239 | switch (sort_by) { 240 | case 'added': 241 | items = _.sortBy(items, function (item) { 242 | return new Date(item.listed_at) 243 | }); 244 | break; 245 | case 'released': 246 | items = _.sortBy(items, function (item) { 247 | return item.released ? new Date(item.released) : new Date(item.first_aired) 248 | }); 249 | break; 250 | case 'popularity': 251 | items = _.sortBy(items, function (item) { 252 | return item.votes*item.rating; 253 | }); 254 | break; 255 | case 'random': 256 | items = _.shuffle(items); 257 | break; 258 | case 'rank': 259 | items = _.sortBy(items, 'rank').reverse(); 260 | break; 261 | case 'title': 262 | items = _.sortBy(items, function (item) { 263 | str = item.title.toLowerCase(); 264 | words = str.split(' '); 265 | if (words.length <= 1) return str; 266 | if (words[0] == 'a' || words[0] == 'the' || words[0] == 'an') 267 | return words.splice(1).join(' '); 268 | return str; 269 | }).reverse(); 270 | break; 271 | case 'runtime': 272 | items = _.sortBy(items, function (item) { 273 | return item.aired_episodes ? item.aired_episodes*item.runtime:item.runtime; 274 | }); 275 | //items = items = _.sortBy(items, 'runtime'); 276 | break; 277 | case 'votes': 278 | items = items = _.sortBy(items, 'votes'); 279 | break; 280 | case 'rating': 281 | items = items = _.sortBy(items, 'rating'); 282 | break; 283 | } 284 | /* 285 | if (sort_by == 'added') { 286 | items = _.sortBy(items, function (item) { 287 | return new Date(item['listed_at']) 288 | }); 289 | } 290 | else if (sort_by == 'released') { 291 | items = _.sortBy(items, function (item) { 292 | return new Date(item['released']) 293 | }); 294 | } 295 | else if (sort_by == 'popularity') items = _.sortBy(items, 'comment_count'); 296 | else if (sort_by == 'random') items = _.shuffle(items); 297 | else if (sort_by == 'rank') items = _.sortBy(items, 'rank'); 298 | else if (sort_by == 'title') { 299 | items = _.sortBy(items, function (item) { 300 | str = item.title.toLowerCase(); 301 | words = str.split(' '); 302 | if (words.length <= 1) return str; 303 | if (words[0] == 'a' || words[0] == 'the' || words[0] == 'an') 304 | return words.splice(1).join(' '); 305 | return str; 306 | }); 307 | } 308 | else if (sort_by == 'runtime') items = items = _.sortBy(items, 'runtime'); 309 | else if (sort_by == 'votes') items = items = _.sortBy(items, 'votes'); 310 | else if (sort_by == 'rating') items = items = _.sortBy(items, 'rating'); 311 | */ 312 | if (sort_how == 'asc') { 313 | items = items.reverse(); 314 | } 315 | //console.log(items.slice(0,5)); 316 | return items; 317 | } 318 | 319 | async function ConvertToStremio(items = [], RPDBkey = {}) { 320 | if (RPDBkey) RPDBkey.valid = await checkRPDB(RPDBkey); 321 | const metas = []; 322 | //console.log('ConvertToStremio', items.length) 323 | for (let i = 0; i < items.length; i++) { 324 | const item = items[i]; 325 | 326 | if (item.ids && item.type == 'movie' || item.type == 'show') { 327 | const type = item.type == 'movie' ? 'movie' : 'series'; 328 | const images = await getPoster(type, item.ids, RPDBkey); 329 | let meta = { 330 | 'id': item.ids.imdb || ('trakt:' + item.ids.trakt), 331 | 'type': type, 332 | 'name': item.title, 333 | 'poster': images.poster || '', 334 | 'background': images.background || '', 335 | 'releaseInfo': item.year ? item.year.toString() : (item.released?.split('-')[0] ? item.released.split('-')[0] : 'N/A'), 336 | 'description': item.overview || '', 337 | 'genres': item.genres || [], 338 | 'trailers': item.trailer ? [{ source: item.trailer.split('?v=')[1], type: 'Trailer' }] : [] 339 | } 340 | if(meta.type =='movie') meta.behaviorHints={'defaultVideoId': meta.id} 341 | metas.push(meta); 342 | } 343 | } 344 | return metas; 345 | } 346 | 347 | async function getImages(type = String, ids = Object) { 348 | let meta = {}; 349 | if (ids.tmdb) { 350 | const images = await tmdb(type, ids.tmdb); 351 | //console.log(images) 352 | if (images) { 353 | if (images.poster) meta.poster = images.poster; 354 | if (images.background) meta.background = images.background; 355 | } 356 | } 357 | return meta 358 | } 359 | 360 | async function getPoster(type, IDs = {}, RPDBkey = {}) { 361 | //console.log('getPoster',type, IDs,RPDBkey) 362 | 363 | const { trakt, imdb, tmdb, tvdb } = IDs; 364 | const { key, valid, poster, posters, tier } = RPDBkey; 365 | const posterType = poster || 'poster-default'; 366 | 367 | let meta = { 368 | poster:'', 369 | background:'', 370 | } 371 | let idType; 372 | if (imdb) idType = 'imdb'; 373 | else if (tmdb) idType = 'tmdb'; 374 | else if (tvdb) idType = 'tvdb'; 375 | //console.log('idType',idType) 376 | if(!idType) return meta; 377 | 378 | if (key && valid) { 379 | meta.poster = `https://api.ratingposterdb.com/${key}/${idType}/${posterType}/${IDs[idType]}.jpg?fallback=true`; 380 | if(tier > 2){ 381 | meta.background = `https://api.ratingposterdb.com/${key}/${idType}/backdrop-default/${IDs[idType]}.jpg?fallback=true` 382 | } 383 | } 384 | if (imdb) { 385 | if(!meta.poster) meta.poster = `https://images.metahub.space/poster/small/${imdb}/img`; 386 | if(!meta.background) meta.background = `https://images.metahub.space/background/medium/${imdb}/img`; 387 | } 388 | //console.log('trakt',trakt,'tmdb',tmdb&&(!meta.poster || !meta.background)) 389 | if(tmdb &&(!meta.poster || !meta.background)){ 390 | const images = await tmdbMeta(type, tmdb); 391 | //console.log(images); 392 | if (images) { 393 | if (!meta.poster && images.poster) meta.poster = images.poster; 394 | if (!meta.background && images.background) meta.background = images.background; 395 | } 396 | } 397 | return meta; 398 | } 399 | 400 | async function checkRPDB(RPDBkey = {}) { 401 | let valid = false; 402 | try { 403 | validate = await client.get(`https://api.ratingposterdb.com/${RPDBkey.key}/isValid`) 404 | if (validate?.data?.valid) valid = validate.data.valid; 405 | else valid = false; 406 | } catch (e) { 407 | valid = false; 408 | } 409 | 410 | return valid; 411 | 412 | } 413 | 414 | function NormalizeLists(list = [], type = String) { 415 | const new_list = []; 416 | 417 | for (let i = 0; i < list.length; i++) { 418 | let new_element = {}; 419 | const element = list[i]; 420 | const keys = Object.keys(element); 421 | for (let keyid in keys) { 422 | let key = keys[keyid]; 423 | if (key == element.type || key == type) { 424 | let subelement = element[key]; 425 | const subkeys = Object.keys(subelement); 426 | for (let subkeyid = 0; subkeyid < subkeys.length; subkeyid++) { 427 | let subkey = subkeys[subkeyid]; 428 | new_element[subkey] = subelement[subkey]; 429 | } 430 | } else { 431 | new_element[key] = element[key]; 432 | } 433 | } 434 | if (!new_element.type && type) new_element.type = type; 435 | new_list.push(new_element); 436 | } 437 | return new_list; 438 | } 439 | 440 | function getUserProfile(access_token){ 441 | return client.get('/users/me',{ headers: { 'Authorization': `Bearer ${access_token}` } }) 442 | 443 | } 444 | async function getToken({code ,refresh_token}) { 445 | let data = { 446 | 'client_id': config.client_id, 447 | 'client_secret': config.client_secret, 448 | 'redirect_uri': myurl, 449 | }; 450 | if(code) { 451 | data.code = code; 452 | data.grant_type = 'authorization_code'; 453 | } 454 | else if(refresh_token){ 455 | data.refresh_token = refresh_token; 456 | data.grant_type = 'refresh_token'; 457 | }else{ 458 | throw 'code or refresh_token is required'; 459 | } 460 | 461 | return client.post(`/oauth/token`, data) 462 | } 463 | 464 | async function listOfLists(query = String, token) { 465 | try { 466 | const popular = []; 467 | let url, header; 468 | if (query == 'trending' || query == 'popular') url = `/lists/${query}/?limit=20`; 469 | else if (query == 'personal') { 470 | if (token) { 471 | url = `/users/me/lists`; 472 | header = { headers: { 'Authorization': `Bearer ${token}` } } 473 | } 474 | else return; 475 | } 476 | else url = `/search/list/?query=${query}`; 477 | //console.log(url, header) 478 | 479 | data = await request(url, header); 480 | if (!data || !data.data) throw 'error'; 481 | for (let i = 0; i < data.data.length; i++) { 482 | const list = data.data[i].list ? data.data[i].list : data.data[i]; 483 | if (list && ((list.privacy == 'public') || token)) { 484 | popular.push({ 485 | name: list.name, 486 | id: list.ids.trakt, 487 | user: list.user.ids.slug ? list.user.ids.slug : list.user.username, 488 | slug: list.ids.slug, 489 | likes: list.likes, 490 | item_count: list.item_count, 491 | description: list.description, 492 | sort: list.sort_by + ',' + list.sort_how 493 | }); 494 | } 495 | } 496 | return popular; 497 | 498 | } catch (e) { 499 | console.error(e); 500 | } 501 | } 502 | 503 | function list(list_ids = [], access_token) { 504 | let header; 505 | //console.log('list_ids', list_ids) 506 | if (access_token) header = { headers: { 'Authorization': `Bearer ${access_token}` } } 507 | const promises = []; 508 | 509 | for (let i = 0; i < list_ids.length; i++) { 510 | let id = list_ids[i]; 511 | if (id.startsWith('trakt_list:')) id = id.replace('trakt_list:', '') 512 | //console.log('id', id) 513 | const { url, sort } = idSplit(id); 514 | 515 | if (url) promises.push(request(url, header).then(data => { if (sort) { data.data.sort = sort }; return data })); 516 | } 517 | 518 | return promises; 519 | 520 | } 521 | 522 | function idSplit(id = String) { 523 | 524 | let list_id, sort, url, user_id; 525 | 526 | user_id = id.split(':')[0]; 527 | list_id = id.split(':')[1]; 528 | sort = id.split(':')[2].split(','); 529 | url = `/users/${user_id}/lists/${list_id}/`; 530 | //console.log(id, sort) 531 | return { list_id: list_id, user_id: user_id, sort: sort || [], url: url } 532 | 533 | } 534 | 535 | function filter(list = Array) { 536 | let result = list.filter( 537 | (element, index) => index === list.findIndex( 538 | other => element.id === other.id 539 | //&& element.lastname === other.lastname 540 | )); 541 | return result; 542 | } 543 | 544 | async function list_cat(ids, access_token) { 545 | return Promise.allSettled(list(ids, access_token)).then(responses => { 546 | const promises = []; 547 | responses.forEach(res=>{ 548 | if (res?.status == 'fulfilled') { 549 | let data = res.value.data; 550 | let name = data.name; 551 | let id = data.ids.trakt || data.ids.slug; 552 | let username = data.user.ids.slug || data.user.username; 553 | let sort = data.sort; 554 | let url, header; 555 | if (data.privacy !== 'public' || data.user.private) url = `/users/${username}/lists/${id}/items`; 556 | else url = `/lists/${id}/items`; 557 | if (access_token) header = { headers: { 'Authorization': `Bearer ${access_token}` } } 558 | 559 | promises.push(request(url, header).then(data => { 560 | if (data && data.data && data.data.length) { 561 | if (username) list_id = (sort && sort.length) ? `trakt_list:${username}:${id}:${sort}` : `trakt_list:${username}:${id}`; 562 | else list_id = (sort && sort.length) ? `trakt_list:${id}:${sort}` : `trakt_list:${id}`; 563 | 564 | return { 565 | 'type': 'trakt', 566 | 567 | 'id': list_id, 568 | 569 | 'name': name, 570 | 571 | 'extra': [{ 'name': 'genre', 'isRequired': false, 'options': sort_array }, { 'name': 'skip', 'isRequired': false }] 572 | } 573 | } 574 | })); 575 | } 576 | }) 577 | 578 | return Promise.allSettled(promises).then(catalogs => { 579 | 580 | 581 | catalogs.filter((element, index, arr) => { 582 | //console.log('element',arr[index]) 583 | if (element.status == 'fulfilled') el = element.value 584 | else el = undefined 585 | catalogs[index] = el; 586 | }); 587 | 588 | catalogs = catalogs.filter(Boolean); 589 | return (catalogs); 590 | }); 591 | }).catch(error => { console.error(error) }) 592 | } 593 | 594 | async function getMeta(type = String, id = String) { 595 | try { 596 | //console.log(type, id); 597 | let url; 598 | if (type == 'movie') url = `/movies/${id}?extended=full` 599 | if (type == 'series') url = `/shows/${id}?extended=full` 600 | if (!url) throw 'error creating url'; 601 | 602 | const data = await request(url); 603 | if (!data || !data.data) throw 'error getting data (getMeta)'; 604 | const item = data.data; 605 | let meta = { 606 | id: id, 607 | type: type, 608 | name: item.title, 609 | genres: item.genres, 610 | description: item.overview, 611 | runtime: item.runtime, 612 | releaseInfo: item.year, 613 | imdbRating: item.rating, 614 | language: item.language, 615 | country: item.country, 616 | website: item.homepage 617 | } 618 | const ids = item.ids; 619 | if (type == 'series') { 620 | const videos = []; 621 | const url = `/shows/${id}/seasons?extended=episodes`; 622 | const data = await request(url); 623 | if (!data || !data.data) throw 'error getting data (getMeta)'; 624 | data.data.forEach(function (season, index, array) { 625 | //console.log('element',season); 626 | season.episodes.forEach(function (episode, index, array) { 627 | videos.push({ id: `trakt:${id}:${episode.ids.trakt}:${episode.season}:${episode.number}`, title: episode.title, episode: episode.number, season: episode.season }) 628 | }) 629 | }) 630 | meta.videos = videos; 631 | } 632 | return meta; 633 | } catch (e) { 634 | console.error(e); 635 | } 636 | } 637 | 638 | 639 | module.exports = { getToken, generic_lists: { watchlist, rec: recomendations, popular, trending }, list_catalog, list_cat, listOfLists, getMeta, search, getUserProfile }; -------------------------------------------------------------------------------- /vue/.env.development: -------------------------------------------------------------------------------- 1 | VITE_APP_URL=http://127.0.0.1:63355 -------------------------------------------------------------------------------- /vue/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist-ssr 12 | *.local 13 | 14 | # Editor directories and files 15 | .vscode/* 16 | !.vscode/extensions.json 17 | .idea 18 | .DS_Store 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /vue/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar"] 3 | } 4 | -------------------------------------------------------------------------------- /vue/README.md: -------------------------------------------------------------------------------- 1 | # Vue 3 + Vite 2 | 3 | This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 ` 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /vue/dist/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dexter21767/Trakt/2596760f9bf937ffb1b0089b02dcd07b6b22a2c1/vue/dist/logo.png -------------------------------------------------------------------------------- /vue/dist/logoPS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dexter21767/Trakt/2596760f9bf937ffb1b0089b02dcd07b6b22a2c1/vue/dist/logoPS.png -------------------------------------------------------------------------------- /vue/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite --host", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@vueuse/head": "^0.7.10", 13 | "axios": "^0.27.2", 14 | "buffer": "^6.0.3", 15 | "flowbite": "^1.5.3", 16 | "jquery": "^3.6.1", 17 | "vue": "^3.2.37", 18 | "vue-dropdowns": "^1.1.2", 19 | "vue-toggles": "^2.1.0", 20 | "vuedraggable": "^4.1.0" 21 | }, 22 | "devDependencies": { 23 | "@vitejs/plugin-vue": "^3.1.0", 24 | "autoprefixer": "^10.4.8", 25 | "postcss": "^8.4.16", 26 | "tailwindcss": "^3.1.8", 27 | "vite": "^3.1.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /vue/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'tailwindcss/nesting': {}, 4 | tailwindcss: {}, 5 | autoprefixer: {}, 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /vue/public/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dexter21767/Trakt/2596760f9bf937ffb1b0089b02dcd07b6b22a2c1/vue/public/background.png -------------------------------------------------------------------------------- /vue/public/clipboard.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | copy-to-clipboard-line 4 | 5 | 6 | -------------------------------------------------------------------------------- /vue/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dexter21767/Trakt/2596760f9bf937ffb1b0089b02dcd07b6b22a2c1/vue/public/logo.png -------------------------------------------------------------------------------- /vue/public/logoPS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dexter21767/Trakt/2596760f9bf937ffb1b0089b02dcd07b6b22a2c1/vue/public/logoPS.png -------------------------------------------------------------------------------- /vue/src/App.vue: -------------------------------------------------------------------------------- 1 | 693 | 694 | 1094 | 1095 | 1096 | -------------------------------------------------------------------------------- /vue/src/components/SearchModal.vue: -------------------------------------------------------------------------------- 1 | 81 | -------------------------------------------------------------------------------- /vue/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import { createHead } from "@vueuse/head" 3 | import './style.css' 4 | import App from './App.vue' 5 | import 'flowbite' 6 | const app = createApp(App) 7 | const head = createHead() 8 | 9 | app.use(head) 10 | 11 | app.mount('#app') 12 | -------------------------------------------------------------------------------- /vue/src/style.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /vue/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{vue,js,ts,jsx,tsx}", 6 | "./node_modules/flowbite/**/*.js", 7 | ], 8 | theme: { 9 | extend: {}, 10 | }, 11 | plugins: [ 12 | require('flowbite/plugin'), 13 | ], 14 | } 15 | -------------------------------------------------------------------------------- /vue/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [vue()] 7 | }) 8 | --------------------------------------------------------------------------------