diff --git a/package-lock.json b/package-lock.json index 4381f359..d16677ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "async": "^3.2.0", "axios": "^0.21.1", + "basic-ftp": "^5.0.3", "bcrypt": "^5.0.1", "fastest-levenshtein": "^1.0.12", "fluent-ffmpeg": "^2.1.2", @@ -2799,6 +2800,14 @@ "node": "^4.5.0 || >= 5.9" } }, + "node_modules/basic-ftp": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.3.tgz", + "integrity": "sha512-QHX8HLlncOLpy54mh+k/sWIFd0ThmRqwe9ZjELybGZK+tZ8rUb9VO0saKJUROTbE+KhzDUT7xziGpGrW8Kmd+g==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/bcrypt": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.0.tgz", @@ -11575,6 +11584,11 @@ "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" }, + "basic-ftp": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.3.tgz", + "integrity": "sha512-QHX8HLlncOLpy54mh+k/sWIFd0ThmRqwe9ZjELybGZK+tZ8rUb9VO0saKJUROTbE+KhzDUT7xziGpGrW8Kmd+g==" + }, "bcrypt": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.0.tgz", diff --git a/package.json b/package.json index 2f2637d9..a4bc1673 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "dependencies": { "async": "^3.2.0", "axios": "^0.21.1", + "basic-ftp": "^5.0.3", "bcrypt": "^5.0.1", "fastest-levenshtein": "^1.0.12", "fluent-ffmpeg": "^2.1.2", @@ -43,6 +44,8 @@ "neo-blessed": "^0.2.0", "node-rsa": "^1.1.1", "node-tvdb": "^4.1.0", + "node-uuid": "^1.4.8", + "primus": "^7.3.5", "recursive-readdir": "^2.2.2", "restify": "^9.0.0-rc.3", "restify-cors-middleware2": "^2.2.0", diff --git a/res/config.json b/res/config.json index 5c647e8e..14bce0e6 100644 --- a/res/config.json +++ b/res/config.json @@ -127,6 +127,9 @@ } } }, + "seedboxImport": { + "concurrency": 1 + }, "seedboxes": [ { "name": "Main seedbox", diff --git a/src/lib/embyEmulation/ServerAPI/index.js b/src/lib/embyEmulation/ServerAPI/index.js new file mode 100644 index 00000000..160975ff --- /dev/null +++ b/src/lib/embyEmulation/ServerAPI/index.js @@ -0,0 +1,65 @@ +import restify from 'restify'; +import routes from './routes'; +import corsMiddleware from 'restify-cors-middleware2'; + +export default class EmbyServerAPI { + /** + * @param {EmbyEmulation} embyEmulation + */ + constructor(embyEmulation) { + this.embyEmulation = embyEmulation; + + // Initialize REST based server + this.server = restify.createServer(); + + // Allow remote clients to connect to the backend + const cors = corsMiddleware({ + preflightMaxAge: 5, // Optional + origins: ['*'], + allowHeaders: ['API-Token'], + exposeHeaders: ['API-Token-Expiry'] + }); + + this.server.pre(cors.preflight); + this.server.use(cors.actual); + + this.server.use(restify.plugins.authorizationParser()); + this.server.use(restify.plugins.queryParser({ mapParams: true })); + this.server.use(restify.plugins.bodyParser({ mapParams: true })); + + this.server.pre(function(req, res, next) { + req.url = req.url.toLowerCase(); + next(); + }); + + this.server.pre(function(req, res, next) { + if (!req.headers['x-emby-authorization']) return next(); + + req.headers['emby'] = req.headers['x-emby-authorization'].split(', '); + + let auth = {}; + + for (let i in req.headers['emby']) { + let item = req.headers['emby'][i].split('='); + + auth[item[0]] = item[1].replace(/"/g, ''); + } + + req.headers['emby'] = auth; + + next(); + }); + + this.server.use(async function (request, response) { + console.log(request.url, request.params, request.method); + }); + + // Add routes routes + routes(this.server, this.embyEmulation); + + // Start restify server + this.server.listen(8096, () => { + console.log('Jellyfin emulation server listening at %s', this.server.url); + }); + } +} diff --git a/src/lib/embyEmulation/ServerAPI/routes/branding/index.js b/src/lib/embyEmulation/ServerAPI/routes/branding/index.js new file mode 100644 index 00000000..e949d77c --- /dev/null +++ b/src/lib/embyEmulation/ServerAPI/routes/branding/index.js @@ -0,0 +1,12 @@ +export default (server, embyEmulation) => { + server.get('/branding/configuration', async (req, res) => { + res.send({ + LoginDisclaimer: 'This is an Oblecto Media server', + CustomCss: '' + }); + }); + + server.get('/branding/css', async (req, res) => { + res.send(); + }); +}; diff --git a/src/lib/embyEmulation/ServerAPI/routes/displaypreferences/index.js b/src/lib/embyEmulation/ServerAPI/routes/displaypreferences/index.js new file mode 100644 index 00000000..002a1305 --- /dev/null +++ b/src/lib/embyEmulation/ServerAPI/routes/displaypreferences/index.js @@ -0,0 +1,32 @@ +/** + * @param {*} server + * @param {EmbyEmulation} embyEmulation + */ +export default (server, embyEmulation) => { + server.get('/displaypreferences/usersettings', async (req, res) => { + res.send( + { + 'Id':'3ce5b65d-e116-d731-65d1-efc4a30ec35c', + 'SortBy':'SortName', + 'RememberIndexing':false, + 'PrimaryImageHeight':250, + 'PrimaryImageWidth':250, + 'CustomPrefs':{ + 'chromecastVersion':'stable','skipForwardLength':'30000','skipBackLength':'10000','enableNextVideoInfoOverlay':'False','tvhome':null,'dashboardTheme':null,'http://192.168.176.55:30013/web/index.htmlmoviecollections':'{\u0022SortBy\u0022:\u0022SortName\u0022,\u0022SortOrder\u0022:\u0022Ascending\u0022}','http://192.168.176.55:30013/web/index.htmlseries':'{\u0022SortBy\u0022:\u0022SortName\u0022,\u0022SortOrder\u0022:\u0022Ascending\u0022}','file:///app/share/jellyfinmediaplayer/web-client/desktop/index.htmltrailers':'{\u0022SortBy\u0022:\u0022SortName\u0022,\u0022SortOrder\u0022:\u0022Ascending\u0022}','file:///app/share/jellyfinmediaplayer/web-client/desktop/index.htmlseries':'{\u0022SortBy\u0022:\u0022SortName\u0022,\u0022SortOrder\u0022:\u0022Ascending\u0022}' + }, + 'ScrollDirection':'Horizontal', + 'ShowBackdrop':true, + 'RememberSorting':false, + 'SortOrder':'Ascending', + 'ShowSidebar':false, + 'Client':'emby' + }); + }); + + server.get('/LiveTv/Programs/Recommended', async (req, res) => { + res.send( + { + 'Items':[],'TotalRecordCount':0,'StartIndex':0 + }); + }); +}; diff --git a/src/lib/embyEmulation/ServerAPI/routes/index.js b/src/lib/embyEmulation/ServerAPI/routes/index.js new file mode 100644 index 00000000..ca1985d9 --- /dev/null +++ b/src/lib/embyEmulation/ServerAPI/routes/index.js @@ -0,0 +1,24 @@ +import system from './system'; +import users from './users'; +import sessions from './sessions'; +import displaypreferences from './displaypreferences'; +import branding from './branding'; +import shows from './shows'; +import items from './items'; +import videos from './videos'; + +/** + * + * @param server + * @param {EmbyEmulation} embyEmulation + */ +export default (server, embyEmulation) => { + system(server, embyEmulation); + users(server, embyEmulation); + sessions(server, embyEmulation); + displaypreferences(server, embyEmulation); + branding(server, embyEmulation); + shows(server, embyEmulation); + items(server, embyEmulation); + videos(server, embyEmulation); +}; diff --git a/src/lib/embyEmulation/ServerAPI/routes/items/index.js b/src/lib/embyEmulation/ServerAPI/routes/items/index.js new file mode 100644 index 00000000..61bc54a1 --- /dev/null +++ b/src/lib/embyEmulation/ServerAPI/routes/items/index.js @@ -0,0 +1,288 @@ +import { Movie } from '../../../../../models/movie'; +import { File } from '../../../../../models/file'; +import { promises as fs } from 'fs'; +import errors from 'restify-errors'; +import { createStreamsList } from '../../../helpers'; +import { Stream } from '../../../../../models/stream'; +import { Op } from 'sequelize'; +import { Series } from '../../../../../models/series'; + +/** + * + * @param server + * @param {EmbyEmulation} embyEmulation + */ +export default (server, embyEmulation) => { + server.get('/items', async (req, res) => { + let items = []; + + if (req.params.includeitemtypes === 'movie') { + let count = await Movie.count(); + + let offset = parseInt(req.params.startindex) | 0; + + let where = null; + + if (req.params.searchterm) { + where = { movieName: { [Op.like]: `%${req.params.searchterm}%` } }; + } + + let results = await Movie.findAll({ + where, + limit: parseInt(req.params.limit) || 100, + offset + }); + + for (let movie of results) { + items.push({ + 'Name': movie.movieName, + 'ServerId': embyEmulation.serverId, + 'Id': 'movie' + movie.id, + 'HasSubtitles': false, + 'Container': 'mkv,webm', + 'PremiereDate': movie.releaseDate, + 'CriticRating': 82, + 'OfficialRating': 'PG-13', + 'CommunityRating': 2.6, + 'RunTimeTicks': movie.runtime*100000000, + 'ProductionYear': movie.releaseDate.substring(0, 4), + 'IsFolder': false, + 'Type': 'Movie', + 'PrimaryImageAspectRatio': 0.6666666666666666, + 'VideoType': 'VideoFile', + 'LocationType': 'FileSystem', + 'MediaType': 'Video', + 'UserData': { + 'PlaybackPositionTicks': 0, + 'PlayCount': 0, + 'IsFavorite': true, + 'Played': false, + 'Key': '337401' + }, + 'ImageTags': { 'Primary': 'eaaa9ab0189f4166db1012ec5230c7db' } + }); + } + + res.send({ + 'Items': items, + 'TotalRecordCount': count, + 'StartIndex': offset + }); + } else if (req.params.includeitemtypes === 'series') { + let count = await Series.count(); + + let offset = parseInt(req.params.startindex) | 0; + + let where = null; + + if (req.params.searchterm) { + where = { seriesName: { [Op.like]: `%${req.params.searchterm}%` } }; + } + + let results = await Series.findAll({ + where, + limit: parseInt(req.params.limit) || 100, + offset + }); + + for (let series of results) { + items.push({ + 'Name': series.seriesName, + 'ServerId':'70301c9fe99e4304bd5c8922b0e2fd90', + 'Id': `series${series.id}`, + 'PremiereDate': series.firstAired, + 'OfficialRating':'TV-14', + 'ChannelId':null, + 'CommunityRating':series.popularity, + 'RunTimeTicks': series.runtime * 100000000, + 'ProductionYear': series.firstAired.substring(0, 4), + 'IsFolder':true, + 'Type':'Series', + 'UserData':{ + 'UnplayedItemCount':13,'PlaybackPositionTicks':0,'PlayCount':0,'IsFavorite':false,'Played':false,'Key':'272644' + }, + 'Status': series.status, + 'AirDays':[series.airsDayOfWeek], + 'PrimaryImageAspectRatio':0.6666666666666666, + 'ImageTags':{ 'Primary':'d4ded7fd31f038b434148a4e162e031d' }, + 'BackdropImageTags':['64e8f381684663b6a7f0d2c1cac61d08'], + 'ImageBlurHashes':{ 'Backdrop':{ '64e8f381684663b6a7f0d2c1cac61d08':'WU7xLXWARPt8V?f,%jRhROt7V?fmx_RiRiogackCtTaxaykCackC' },'Primary':{ 'd4ded7fd31f038b434148a4e162e031d':'d23[JJxG9rW=;LbHS$sT*^n%Tfn$TfW:aIniX:bIRhbb' } }, + 'LocationType':'FileSystem', + 'EndDate':null + }); + } + + res.send({ + 'Items': items, + 'TotalRecordCount': count, + 'StartIndex': offset + }); + } else { + res.send({ + Items: [], + TotalRecordCount: 0, + StartIndex: 0 + }); + } + }); + + server.get('/items/:mediaid/similar', async (req, res) => { + res.send({ + Items: [], + TotalRecordCount: 0, + StartIndex: 0 + }); + }); + + server.get('/items/:mediaid/thememedia', async (req, res) => { + res.send({ + 'ThemeVideosResult': { + // 'OwnerId': 'f27caa37e5142225cceded48f6553502', + 'Items': [], + 'TotalRecordCount': 0, + 'StartIndex': 0 + }, + 'ThemeSongsResult': { + // 'OwnerId': 'f27caa37e5142225cceded48f6553502', + 'Items': [], + 'TotalRecordCount': 0, + 'StartIndex': 0 + }, + 'SoundtrackSongsResult': { + 'Items': [], + 'TotalRecordCount': 0, + 'StartIndex': 0 + } + }); + }); + + server.get('/items/:mediaid/images/primary', async (req, res) => { + let mediaid = req.params.mediaid; + + if (mediaid.includes('movie')) { + let movie = await Movie.findByPk(mediaid.replace('movie', ''), { include: [File] }); + + let posterPath = embyEmulation.oblecto.artworkUtils.moviePosterPath(movie, 'medium'); + + res.sendRaw(await fs.readFile(posterPath)); + } + + if (mediaid.includes('series')) { + let series = await Series.findByPk(mediaid.replace('series', ''), { include: [File] }); + + let posterPath = embyEmulation.oblecto.artworkUtils.moviePosterPath(series, 'medium'); + + res.sendRaw(await fs.readFile(posterPath)); + } + }); + + server.get('/items/:mediaid/images/backdrop/:artworkid', async (req, res) => { + let mediaid = req.params.mediaid; + + if (mediaid.includes('movie')) { + let movie = await Movie.findByPk(mediaid.replace('movie', ''), { include: [File] }); + + let posterPath = embyEmulation.oblecto.artworkUtils.movieFanartPath(movie, 'large'); + + res.sendRaw(await fs.readFile(posterPath)); + } + + if (mediaid.includes('series')) { + let series = await Movie.findByPk(mediaid.replace('series', ''), { include: [File] }); + + let posterPath = embyEmulation.oblecto.artworkUtils.movieFanartPath(series, 'large'); + + res.sendRaw(await fs.readFile(posterPath)); + } + }); + + server.post('/items/:mediaid/playbackinfo', async (req, res) => { + let mediaid = req.params.mediaid; + + let files = []; + + if (mediaid.includes('movie')) { + let movie = await Movie.findByPk(req.params.mediaid.replace('movie', ''), { + include: [ + { + model: File, + include: [{ model: Stream }], + } + ] + }); + + files = movie.Files; + } + else if (mediaid.includes('series')) { + let series = await Series.findByPk(req.params.mediaid.replace('series', ''), { + include: [ + { + model: File, + include: [{ model: Stream }], + } + ] + }); + + files = series.Files; + } + + let file = files[0]; + + if (req.params.MediaSourceId) { + for (file of files) { + if (file.id === req.params.MediaSourceId) { + break; + } + } + } + + const streamSession = embyEmulation.oblecto.streamSessionController.newSession(file, + { + streamType: 'directhttp', + target: { + formats: ['mp4, mkv'], videoCodecs: ['h264', 'hevc'], audioCodecs: [] + } + }); + + res.send({ + 'MediaSources': files.map((file) => { + return { + 'Protocol':'File', + 'Id': file.id, + 'Path':file.path, + 'Type':'Default', + 'Container':'mkv', + 'Size':file.size, + 'Name':file.name, + 'IsRemote':false, + 'ETag':'3670b404eb5adec1d6cd73868ad1801c', + 'RunTimeTicks':file.duration*10000000, + 'ReadAtNativeFramerate':false, + 'IgnoreDts':false, + 'IgnoreIndex':false, + 'GenPtsInput':false, + 'SupportsTranscoding':true, + 'SupportsDirectStream':true, + 'SupportsDirectPlay':true, + 'IsInfiniteStream':false, + 'RequiresOpening':false, + 'RequiresClosing':false, + 'RequiresLooping':false, + 'SupportsProbing':true, + 'VideoType':'VideoFile', + 'MediaStreams':createStreamsList(file.Streams), + 'MediaAttachments':[], + 'Formats':[], + 'Bitrate':file.size/file.duration, + 'RequiredHttpHeaders':{}, + 'DefaultAudioStreamIndex':1, + 'DefaultSubtitleStreamIndex':2 + }; + }), + 'PlaySessionId':streamSession.sessionId + }); + + console.log('playback info complete'); + }); + +}; diff --git a/src/lib/embyEmulation/ServerAPI/routes/sessions/index.js b/src/lib/embyEmulation/ServerAPI/routes/sessions/index.js new file mode 100644 index 00000000..b908218d --- /dev/null +++ b/src/lib/embyEmulation/ServerAPI/routes/sessions/index.js @@ -0,0 +1,24 @@ +/** + * @param {*} server + * @param {EmbyEmulation} embyEmulation + */ +export default (server, embyEmulation) => { + server.post('/sessions/capabilities/:type', async (req, res) => { + embyEmulation.sessions[req.headers.emby.Token].capabilities = req.params; + + res.send(); + }); + + server.post('/sessions/playing', async (req, res) => { + embyEmulation.sessions[req.headers.emby.Token].playSession = req.params; + + console.log(req.params); + + embyEmulation.websocketSessions[req.headers.emby.Token].write({ + MessageType: 'Play', + Data: req.params + }); + + res.send(); + }); +}; diff --git a/src/lib/embyEmulation/ServerAPI/routes/shows/index.js b/src/lib/embyEmulation/ServerAPI/routes/shows/index.js new file mode 100644 index 00000000..21ac0b47 --- /dev/null +++ b/src/lib/embyEmulation/ServerAPI/routes/shows/index.js @@ -0,0 +1,17 @@ +/** + * @param {*} server + * @param {EmbyEmulation} embyEmulation + */ +export default (server, embyEmulation) => { + server.get('/shows/nextup', async (req, res) => { + res.send({ + 'Items':[],'TotalRecordCount':0,'StartIndex':0 + }); + }); + + server.get('/shows/:seriesid/episodes', async (req, res) => { + res.send({ + 'Items':[],'TotalRecordCount':0,'StartIndex':0 + }); + }); +}; diff --git a/src/lib/embyEmulation/ServerAPI/routes/system/index.js b/src/lib/embyEmulation/ServerAPI/routes/system/index.js new file mode 100644 index 00000000..041b7399 --- /dev/null +++ b/src/lib/embyEmulation/ServerAPI/routes/system/index.js @@ -0,0 +1,39 @@ +import ping from './ping'; +import info from './info'; + +export default (server, embyEmulation) => { + ping(server, embyEmulation); + info(server, embyEmulation); + + server.get('/system/endpoint', async (req, res) => { + res.send({ + IsLocal: true, + IsInNetwork: true + }); + }); + + server.get('/System/ActivityLog/Entries', async (req, res) => { + res.send({ + 'Items':[ + { + 'Id':73,'Name':'robin is online from Tria','ShortOverview':'IP address: 192.168.176.23','Type':'SessionStarted','Date':'2023-09-01T21:44:45.6801443Z','UserId':'028c5cba37874cfa99d5c2089ff75599','Severity':'Information' + },{ + 'Id':72,'Name':'robin has disconnected from Tria','ShortOverview':'IP address: 192.168.176.23','Type':'SessionEnded','Date':'2023-09-01T21:44:17.5099232Z','UserId':'028c5cba37874cfa99d5c2089ff75599','Severity':'Information' + },{ + 'Id':71,'Name':'robin has finished playing The Pod Generation on Tria','Type':'VideoPlaybackStopped','Date':'2023-09-01T21:40:16.3516098Z','UserId':'028c5cba37874cfa99d5c2089ff75599','Severity':'Information' + },{ + 'Id':70,'Name':'robin is playing The Pod Generation on Tria','Type':'VideoPlayback','Date':'2023-09-01T21:34:32.5569758Z','UserId':'028c5cba37874cfa99d5c2089ff75599','Severity':'Information' + },{ + 'Id':69,'Name':'robin is online from Tria','ShortOverview':'IP address: 192.168.176.23','Type':'SessionStarted','Date':'2023-09-01T21:34:26.9833616Z','UserId':'028c5cba37874cfa99d5c2089ff75599','Severity':'Information' + },{ + 'Id':68,'Name':'robin is online from Tria','ShortOverview':'IP address: 192.168.176.23','Type':'SessionStarted','Date':'2023-09-01T21:34:26.9754616Z','UserId':'028c5cba37874cfa99d5c2089ff75599','Severity':'Information' + },{ + 'Id':67,'Name':'robin is online from Firefox','ShortOverview':'IP address: 192.168.176.23','Type':'SessionStarted','Date':'2023-09-01T20:57:07.9534326Z','UserId':'028c5cba37874cfa99d5c2089ff75599','Severity':'Information' + } + ], + 'TotalRecordCount':33, + 'StartIndex':0 + }); + }); + +}; diff --git a/src/lib/embyEmulation/ServerAPI/routes/system/info.js b/src/lib/embyEmulation/ServerAPI/routes/system/info.js new file mode 100644 index 00000000..8c5042a8 --- /dev/null +++ b/src/lib/embyEmulation/ServerAPI/routes/system/info.js @@ -0,0 +1,41 @@ +export default (server, embyEmulation) => { + server.get('/system/info/public', async (req, res) => { + res.send({ + 'LocalAddress': 'http://pegasus:9096', + 'ServerName': embyEmulation.serverName, + 'Version': '10.8.10', + 'ProductName': 'Jellyfin Server', + 'OperatingSystem': 'Linux', + 'Id': embyEmulation.serverId, + 'StartupWizardCompleted': true + }); + }); + + server.get('/system/info', async (req, res) => { + res.send({ + 'OperatingSystemDisplayName': 'Linux', + 'HasPendingRestart': false, + 'IsShuttingDown': false, + 'SupportsLibraryMonitor': true, + 'WebSocketPortNumber': 9096, + 'CompletedInstallations': [], + 'CanSelfRestart': false, + 'CanLaunchWebBrowser': false, + 'ProgramDataPath': '/config/data', + 'WebPath': '/usr/share/jellyfin/web', + 'ItemsByNamePath': '/config/data/metadata', + 'CachePath': '/config/cache', + 'LogPath': '/config/log', + 'InternalMetadataPath': '/config/data/metadata', + 'TranscodingTempPath': '/config/data/transcodes', + 'HasUpdateAvailable': false, + 'EncoderLocation': 'Custom', + 'SystemArchitecture': 'X64', + 'LocalAddress': 'http://pegasus:9096', + 'ServerName': embyEmulation.serverName, + 'Version': embyEmulation.version, + 'OperatingSystem': 'Linux', + 'Id': '79d44cdaf63d4e0ab91fca60b8e4b6d6' + }); + }); +}; diff --git a/src/lib/embyEmulation/ServerAPI/routes/system/ping.js b/src/lib/embyEmulation/ServerAPI/routes/system/ping.js new file mode 100644 index 00000000..a6855da9 --- /dev/null +++ b/src/lib/embyEmulation/ServerAPI/routes/system/ping.js @@ -0,0 +1,9 @@ +export default (server, embyEmulation) => { + server.get('/system/ping', async (req, res) => { + res.send(); + }); + + server.post('/system/ping', async (req, res) => { + res.send(); + }); +}; diff --git a/src/lib/embyEmulation/ServerAPI/routes/users/index.js b/src/lib/embyEmulation/ServerAPI/routes/users/index.js new file mode 100644 index 00000000..5e3cb726 --- /dev/null +++ b/src/lib/embyEmulation/ServerAPI/routes/users/index.js @@ -0,0 +1,554 @@ +import { Movie } from '../../../../../models/movie'; +import { TrackMovie } from '../../../../../models/trackMovie'; +import { File } from '../../../../../models/file'; +import { User } from '../../../../../models/user'; +import { Stream } from '../../../../../models/stream'; +import { createStreamsList } from '../../../helpers'; + +/** + * @param {*} server + * @param {EmbyEmulation} embyEmulation + */ +export default (server, embyEmulation) => { + server.get('/users/public', async (req, res) => { + res.send([]); + }); + + server.post('/users/authenticatebyname', async (req, res) => { + let sessionId = await embyEmulation.handleLogin(req.params.Username, req.params.Pw); + + res.send({ + User: embyEmulation.sessions[sessionId], + SessionInfo: {}, + AccessToken: sessionId, + ServerId: embyEmulation.serverId + }); + }); + + server.get('/users/:userid', async (req, res) => { + let user = await User.findByPk(req.params.userid); + + let HasPassword = user.password !== ''; + + res.send({ + Name: user.name, + ServerId: embyEmulation.serverId, + Id: user.id, + HasPassword, + HasConfiguredPassword: HasPassword, + HasConfiguredEasyPassword: false, + EnableAutoLogin: false, + LastLoginDate: '2020-09-11T23:37:27.3042432Z', + LastActivityDate: '2020-09-11T23:37:27.3042432Z', + 'Configuration': { + 'PlayDefaultAudioTrack': true, + 'SubtitleLanguagePreference': '', + 'DisplayMissingEpisodes': false, + 'GroupedFolders': [], + 'SubtitleMode': 'Default', + 'DisplayCollectionsView': false, + 'EnableLocalPassword': false, + 'OrderedViews': [], + 'LatestItemsExcludes': [], + 'MyMediaExcludes': [], + 'HidePlayedInLatest': true, + 'RememberAudioSelections': true, + 'RememberSubtitleSelections': true, + 'EnableNextEpisodeAutoPlay': true + }, + 'Policy': { + 'IsAdministrator': true, + 'IsHidden': true, + 'IsDisabled': false, + 'BlockedTags': [], + 'EnableUserPreferenceAccess': true, + 'AccessSchedules': [], + 'BlockUnratedItems': [], + 'EnableRemoteControlOfOtherUsers': false, + 'EnableSharedDeviceControl': false, + 'EnableRemoteAccess': false, + 'EnableLiveTvManagement': false, + 'EnableLiveTvAccess': false, + 'EnableMediaPlayback': true, + 'EnableAudioPlaybackTranscoding': false, + 'EnableVideoPlaybackTranscoding': false, + 'EnablePlaybackRemuxing': true, + 'ForceRemoteSourceTranscoding': false, + 'EnableContentDeletion': true, + 'EnableContentDeletionFromFolders': [], + 'EnableContentDownloading': true, + 'EnableSyncTranscoding': false, + 'EnableMediaConversion': false, + 'EnabledDevices': [], + 'EnableAllDevices': true, + 'EnabledChannels': [], + 'EnableAllChannels': true, + 'EnabledFolders': [], + 'EnableAllFolders': true, + 'InvalidLoginAttemptCount': 0, + 'LoginAttemptsBeforeLockout': -1, + 'MaxActiveSessions': 0, + 'EnablePublicSharing': true, + 'BlockedMediaFolders': [], + 'BlockedChannels': [], + 'RemoteClientBitrateLimit': 0, + 'AuthenticationProviderId': 'Jellyfin.Server.Implementations.Users.DefaultAuthenticationProvider', + 'PasswordResetProviderId': 'Jellyfin.Server.Implementations.Users.DefaultPasswordResetProvider', + 'SyncPlayAccess': 'CreateAndJoinGroups' + } + }); + }); + + server.get('/users/:userid/views', async (req, res) => { + res.send({ + 'Items': [ + { + 'Name': 'Movies', + 'ServerId': embyEmulation.serverId, + 'Id': 'f137a2dd21bbc1b99aa5c0f6bf02a805', + 'Etag': 'cf36c1cd9bcd03c80bd92c9570ec620b', + 'DateCreated': '2020-08-31T16:25:53.2124461Z', + 'CanDelete': false, + 'CanDownload': false, + 'SortName': 'movies', + 'ExternalUrls': [], + 'Path': '/config/data/root/default/Movies', + 'EnableMediaSourceDisplay': true, + 'Taglines': [], + 'Genres': [], + 'PlayAccess': 'Full', + 'RemoteTrailers': [], + 'ProviderIds': {}, + 'IsFolder': true, + 'ParentId': 'e9d5075a555c1cbc394eec4cef295274', + 'Type': 'CollectionFolder', + 'People': [], + 'Studios': [], + 'GenreItems': [], + 'LocalTrailerCount': 0, + 'UserData': { + 'PlaybackPositionTicks': 0, + 'PlayCount': 0, + 'IsFavorite': false, + 'Played': false, + 'Key': 'f137a2dd-21bb-c1b9-9aa5-c0f6bf02a805' + }, + 'ChildCount': 2, + 'SpecialFeatureCount': 0, + 'DisplayPreferencesId': 'f137a2dd21bbc1b99aa5c0f6bf02a805', + 'Tags': [], + 'PrimaryImageAspectRatio': 1, + 'CollectionType': 'movies', + // 'ImageTags': {'Primary': '8d5abf60711bc8af6ef4063baf6b67e4'}, + 'BackdropImageTags': [], + 'ScreenshotImageTags': [], + // 'ImageBlurHashes': {'Primary': {'8d5abf60711bc8af6ef4063baf6b67e4': 'WvIE5t05-gs,RVt6a%s,axa#fRodETt0WGa#fha$Rot3WBj[oLaf'}}, + 'LocationType': 'FileSystem', + 'LockedFields': [], + 'LockData': false + }, { + 'Name': 'TV Shows', + 'ServerId': embyEmulation.serverId, + 'Id': '767bffe4f11c93ef34b805451a696a4e', + 'Etag': '838cbe93f5d829a9df3df680e4d14065', + 'DateCreated': '2020-08-31T04:36:37.8321784Z', + 'CanDelete': false, + 'CanDownload': false, + 'SortName': 'tv shows', + 'ExternalUrls': [], + 'Path': '/config/data/root/default/TV Shows', + 'EnableMediaSourceDisplay': true, + 'Taglines': [], + 'Genres': [], + 'PlayAccess': 'Full', + 'RemoteTrailers': [], + 'ProviderIds': {}, + 'IsFolder': true, + 'ParentId': 'e9d5075a555c1cbc394eec4cef295274', + 'Type': 'CollectionFolder', + 'People': [], + 'Studios': [], + 'GenreItems': [], + 'LocalTrailerCount': 0, + 'UserData': { + 'PlaybackPositionTicks': 0, + 'PlayCount': 0, + 'IsFavorite': false, + 'Played': false, + 'Key': '767bffe4-f11c-93ef-34b8-05451a696a4e' + }, + 'ChildCount': 9, + 'SpecialFeatureCount': 0, + 'DisplayPreferencesId': '767bffe4f11c93ef34b805451a696a4e', + 'Tags': [], + 'PrimaryImageAspectRatio': 1, + 'CollectionType': 'tvshows', + // 'ImageTags': {'Primary': '12c129f756f9ae7ca28c3d87ac4aa3b5'}, + 'BackdropImageTags': [], + 'ScreenshotImageTags': [], + // 'ImageBlurHashes': {'Primary': {'12c129f756f9ae7ca28c3d87ac4aa3b5': 'WrHeF9~X%gt7e-Rjs.WBoft7xutRR,t7s:aebHofoft7WBWBRjRj'}}, + 'LocationType': 'FileSystem', + 'LockedFields': [], + 'LockData': false + } + ], + 'TotalRecordCount': 2, + 'StartIndex': 0 + }); + }); + + server.get('/users/:userid/items', async (req, res) => { + if (req.params.includeitemtypes === 'movie') { + let count = await Movie.count(); + + let offset = parseInt(req.params.startindex) | 0; + + let results = await Movie.findAll({ + limit: parseInt(req.params.limit) || 100, + offset + }); + + let items = []; + + for (let movie of results) { + items.push({ + 'Name': movie.movieName, + 'ServerId': embyEmulation.serverId, + 'Id': 'movie' + movie.id, + 'HasSubtitles': true, + 'Container': 'mkv,webm', + 'PremiereDate': movie.releaseDate, + 'CriticRating': 82, + 'OfficialRating': 'PG-13', + 'CommunityRating': 2.6, + 'RunTimeTicks': movie.runtime * 10000000, + 'ProductionYear': movie.releaseDate.substring(0, 4), + 'IsFolder': false, + 'Type': 'Movie', + 'PrimaryImageAspectRatio': 0.6666666666666666, + 'VideoType': 'VideoFile', + 'LocationType': 'FileSystem', + 'MediaType': 'Video', + 'UserData': { + 'PlaybackPositionTicks': 0, + 'PlayCount': 0, + 'IsFavorite': true, + 'Played': false, + 'Key': '337401' + }, + 'ImageTags': { 'Primary': 'WhyIsThisEvenNeeded' } + + }); + } + + res.send({ + 'Items': items, 'TotalRecordCount': count, 'StartIndex': offset + }); + } else { + res.send({ + Items: [], + TotalRecordCount: 0, + StartIndex: 0 + }); + } + }); + + server.get('/users/:userid/items/:mediaid', async (req, res) => { + if (req.params.mediaid.includes('movie')) { + let movie = await Movie.findByPk(req.params.mediaid.replace('movie', ''), { include: [{ model: File, include: [{ model: Stream }] }] }); + + let MediaSources = []; + + for (let file of movie.Files) { + + MediaSources.push({ + 'Protocol': 'File', + 'Id': file.id, + 'Path': file.path, + 'Type': 'Default', + 'Container': file.container, + 'Size': file.size, + 'Name': file.name, + 'IsRemote': false, + 'ETag': '313f5f26c5f6636a77c630468b6920f7', + 'RunTimeTicks': file.duration * 10000000, + 'ReadAtNativeFramerate': false, + 'IgnoreDts': false, + 'IgnoreIndex': false, + 'GenPtsInput': false, + 'SupportsTranscoding': true, + 'SupportsDirectStream': true, + 'SupportsDirectPlay': true, + 'IsInfiniteStream': false, + 'RequiresOpening': false, + 'RequiresClosing': false, + 'RequiresLooping': false, + 'SupportsProbing': true, + 'VideoType': 'VideoFile', + 'MediaStreams': createStreamsList(file.Streams), + 'MediaAttachments': [], + 'Formats': [], + 'Bitrate': file.size/file.duration, + 'RequiredHttpHeaders': {}, + 'DefaultAudioStreamIndex': 1, + 'DefaultSubtitleStreamIndex': 2, + }); + } + + res.send({ + 'Name': movie.movieName, + 'OriginalTitle': movie.originalName, + 'ServerId': embyEmulation.serverId, + 'Id': 'movie' + movie.id, + 'Etag': '6448f9c5d2678db5ffa4de1c283f6e6a', + 'DateCreated': movie.createdAt, + 'CanDelete': false, + 'CanDownload': true, + 'HasSubtitles': true, + 'Container': 'mkv,webm', + 'SortName': movie.movieName, + 'PremiereDate': movie.releaseDate, + 'ExternalUrls': [ + { 'Name': 'IMDb', 'Url': `https://www.imdb.com/title/${movie.imdbid}` }, + { 'Name': 'TheMovieDb', 'Url': `https://www.themoviedb.org/movie/${movie.tmdbid}` }, + { 'Name': 'Trakt', 'Url': `https://trakt.tv/movies/${movie.imdbid}` } + ], + 'MediaSources': MediaSources, + 'CriticRating': 82, + 'ProductionLocations': ['China', 'United States of America'], + 'Path': movie.Files[0].path, + 'EnableMediaSourceDisplay': true, + 'OfficialRating': 'PG-13', + 'Overview': movie.overview, + 'Taglines': [movie.tagline], + 'Genres': movie.genres, + 'CommunityRating': 2.6, + 'RunTimeTicks': movie.runtime * 10000000, + 'PlayAccess': 'Full', + 'ProductionYear': movie.releaseDate.substring(0, 4), + 'RemoteTrailers': [], + 'ProviderIds': { 'Tmdb': movie.tmdbid, 'Imdb': movie.imdbid }, + 'IsHD': true, + 'IsFolder': false, + 'ParentId': 'e675012a1892a87530d2c0b0d14a9026', + 'Type': 'Movie', + 'People': [], + 'Studios': [], + 'LocalTrailerCount': 0, + 'UserData': { + 'PlaybackPositionTicks': 0, + 'PlayCount': 0, + 'IsFavorite': true, + 'Played': false, + 'Key': '337401' + }, + 'SpecialFeatureCount': 0, + 'DisplayPreferencesId': 'dbf7709c41faaa746463d67978eb863d', + 'Tags': [], + 'PrimaryImageAspectRatio': 0.6666666666666666, + 'ImageTags': { 'Primary': 'ThisIDisfairlyuseless' }, + 'BackdropImageTags': ['be04a5eac7bc48ea3f5834aa816a03f0'], + 'VideoType': 'VideoFile', + // 'ImageTags': {'Primary': 'eaaa9ab0189f4166db1012ec5230c7db'}, + // 'BackdropImageTags': ['be04a5eac7bc48ea3f5834aa816a03f0'], + 'ScreenshotImageTags': [], + // 'ImageBlurHashes': { + // 'Backdrop': {'be04a5eac7bc48ea3f5834aa816a03f0': 'W7D78hkBL};OCl}E}G,rI:65KOSxITWVx^K39tjG+]sBs;Sgadwd'}, + // 'Primary': {'eaaa9ab0189f4166db1012ec5230c7db': 'ddHoON-V.S%g~qxuxuniRPRjMxM{-;M{Rjoz%#Nasoxa'} + // }, + 'Chapters': [ + { + 'StartPositionTicks': 0, + 'Name': 'Chapter 1', + 'ImageDateModified': '0001-01-01T00:00:00.0000000Z' + }, + { + 'StartPositionTicks': 3000000000, + 'Name': 'Chapter 2', + 'ImageDateModified': '0001-01-01T00:00:00.0000000Z' + }, + { + 'StartPositionTicks': 6000000000, + 'Name': 'Chapter 3', + 'ImageDateModified': '0001-01-01T00:00:00.0000000Z' + }, + { + 'StartPositionTicks': 9000000000, + 'Name': 'Chapter 4', + 'ImageDateModified': '0001-01-01T00:00:00.0000000Z' + }, + { + 'StartPositionTicks': 12000000000, + 'Name': 'Chapter 5', + 'ImageDateModified': '0001-01-01T00:00:00.0000000Z' + }, + { + 'StartPositionTicks': 15000000000, + 'Name': 'Chapter 6', + 'ImageDateModified': '0001-01-01T00:00:00.0000000Z' + }, + { + 'StartPositionTicks': 18000000000, + 'Name': 'Chapter 7', + 'ImageDateModified': '0001-01-01T00:00:00.0000000Z' + }, + { + 'StartPositionTicks': 21000000000, + 'Name': 'Chapter 8', + 'ImageDateModified': '0001-01-01T00:00:00.0000000Z' + }, + { + 'StartPositionTicks': 24000000000, + 'Name': 'Chapter 9', + 'ImageDateModified': '0001-01-01T00:00:00.0000000Z' + }, + { + 'StartPositionTicks': 27000000000, + 'Name': 'Chapter 10', + 'ImageDateModified': '0001-01-01T00:00:00.0000000Z' + }, + { + 'StartPositionTicks': 30000000000, + 'Name': 'Chapter 11', + 'ImageDateModified': '0001-01-01T00:00:00.0000000Z' + }, + { + 'StartPositionTicks': 33000000000, + 'Name': 'Chapter 12', + 'ImageDateModified': '0001-01-01T00:00:00.0000000Z' + }, + { + 'StartPositionTicks': 36000000000, + 'Name': 'Chapter 13', + 'ImageDateModified': '0001-01-01T00:00:00.0000000Z' + }, + { + 'StartPositionTicks': 39000000000, + 'Name': 'Chapter 14', + 'ImageDateModified': '0001-01-01T00:00:00.0000000Z' + }, + { + 'StartPositionTicks': 42000000000, + 'Name': 'Chapter 15', + 'ImageDateModified': '0001-01-01T00:00:00.0000000Z' + }, + { + 'StartPositionTicks': 45000000000, + 'Name': 'Chapter 16', + 'ImageDateModified': '0001-01-01T00:00:00.0000000Z' + }, + { + 'StartPositionTicks': 48000000000, + 'Name': 'Chapter 17', + 'ImageDateModified': '0001-01-01T00:00:00.0000000Z' + }, + { + 'StartPositionTicks': 51000000000, + 'Name': 'Chapter 18', + 'ImageDateModified': '0001-01-01T00:00:00.0000000Z' + }, + { + 'StartPositionTicks': 54000000000, + 'Name': 'Chapter 19', + 'ImageDateModified': '0001-01-01T00:00:00.0000000Z' + }, + { + 'StartPositionTicks': 57000000000, + 'Name': 'Chapter 20', + 'ImageDateModified': '0001-01-01T00:00:00.0000000Z' + }, + { + 'StartPositionTicks': 60000000000, + 'Name': 'Chapter 21', + 'ImageDateModified': '0001-01-01T00:00:00.0000000Z' + }, + { + 'StartPositionTicks': 63000000000, + 'Name': 'Chapter 22', + 'ImageDateModified': '0001-01-01T00:00:00.0000000Z' + }, + { + 'StartPositionTicks': 66000000000, + 'Name': 'Chapter 23', + 'ImageDateModified': '0001-01-01T00:00:00.0000000Z' + } + ], + 'LocationType': 'FileSystem', + 'MediaType': 'Video', + 'LockedFields': [], + 'LockData': false, + 'Width': 1920, + 'Height': 1080, + }); + } else { + res.send({ + Items: [], + TotalRecordCount: 0, + StartIndex: 0 + }); + } + }); + + server.get('/users/:userid/items/:mediaid/intros', async (req, res) => { + res.send({ + 'Items': [], 'TotalRecordCount': 0, 'StartIndex': 0 + }); + }); + + server.get('/users/:userid/items/resume', async (req, res) => { + res.send({ + 'Items': [], 'TotalRecordCount': 0, 'StartIndex': 0 + }); + }); + + server.get('/users/:userid/items/latest', async (req, res) => { + console.log(req.headers); + console.log(embyEmulation.sessions); + + let results = await Movie.findAll({ + include: [ + { + model: TrackMovie, + required: false, + where: { userId: embyEmulation.sessions[req.headers.emby.Token].Id } + } + ], + order: [['releaseDate', 'DESC']], + limit: 50, + offset: 0 + }); + + let movies = results.map((movie) => { + return { + 'Name': movie.movieName, + 'ServerId': embyEmulation.serverId, + 'Id': 'movie' + movie.id, + 'HasSubtitles': true, + 'Container': 'mkv,webm', + 'PremiereDate': movie.releaseDate, + 'CriticRating': 82, + 'OfficialRating': 'PG-13', + 'CommunityRating': 2.6, + 'RunTimeTicks': movie.runtime * 10000000, + 'ProductionYear': movie.releaseDate.substring(0, 4), + 'IsFolder': false, + 'Type': 'Movie', + 'PrimaryImageAspectRatio': 0.6666666666666666, + 'VideoType': 'VideoFile', + 'LocationType': 'FileSystem', + 'MediaType': 'Video', + 'UserData': { + 'PlaybackPositionTicks': 0, + 'PlayCount': 0, + 'IsFavorite': true, + 'Played': false, + 'Key': '337401' + }, + 'ImageTags': { 'Primary': 'WhyIsThisEvenNeeded' } + + }; + }); + + res.send(movies); + }); +}; diff --git a/src/lib/embyEmulation/ServerAPI/routes/videos/index.js b/src/lib/embyEmulation/ServerAPI/routes/videos/index.js new file mode 100644 index 00000000..03e9a7fd --- /dev/null +++ b/src/lib/embyEmulation/ServerAPI/routes/videos/index.js @@ -0,0 +1,18 @@ +import { Movie } from '../../../../../models/movie'; +import { File } from '../../../../../models/file'; +import { Stream } from '../../../../../models/stream'; +import errors from 'restify-errors'; +import DirectHttpStreamSession from '../../../../streamSessions/StreamSessionTypes/DirectHttpStreamSession'; + +export default (server, embyEmulation) => { + server.get('/videos/:mediaid/stream.:ext', async (req, res) => { + // if (!embyEmulation.oblecto.streamSessionController.sessionExists(req.params.mediasourceid)) { + // console.log('The stream session doesn\'t exist'); + // return new errors.InvalidCredentialsError('Stream session token does not exist'); + // } + + const file = await File.findByPk(req.params.mediasourceid); + + await DirectHttpStreamSession.httpStreamHandler(req, res, file); + }); +}; diff --git a/src/lib/embyEmulation/helpers.js b/src/lib/embyEmulation/helpers.js new file mode 100644 index 00000000..5834d038 --- /dev/null +++ b/src/lib/embyEmulation/helpers.js @@ -0,0 +1,86 @@ +export const createStreamsList = (streams) => { + let mediaStreams = []; + + for (const stream of streams) { + switch (stream.codec_type) { + case 'video': + mediaStreams.push({ + 'Codec': stream.codec_name, + 'Language': stream.tags_language || 'eng', + 'ColorTransfer': stream.color_transfer, + 'ColorPrimaries': stream.color_primaries, + 'TimeBase': stream.time_base, + 'CodecTimeBase': stream.codec_time_base, + 'VideoRange': stream.color_range, + 'DisplayTitle': stream.tags_title || `${stream.width}p ${stream.codec_name} ${stream.color_range}`, + 'NalLengthSize': '0', + 'IsInterlaced': false, + 'IsAVC': false, + 'BitRate': 9253220, + 'BitDepth': 8, + 'RefFrames': 1, + 'IsDefault': true, + 'IsForced': false, + 'Height': stream.height, + 'Width': stream.width, + 'AverageFrameRate': 23.976025, + 'RealFrameRate': 23.976025, + 'Profile': 'High', + 'Type': 'Video', + 'AspectRatio': stream.display_aspect_ratio, + 'Index': stream.index, + 'IsExternal': false, + 'IsTextSubtitleStream': false, + 'SupportsExternalStream': false, + 'PixelFormat': stream.pix_fmt, + 'Level': 40 + }); + break; + case 'audio': + mediaStreams.push({ + 'Codec': stream.codec_name, + 'Language': stream.tags_language, + 'TimeBase': stream.time_base, + 'CodecTimeBase': stream.codec_time_base, + 'Title': stream.tags_title || stream.tags_language, + 'DisplayTitle': stream.tags_title || `${stream.tags_language} ${stream.codec_name}`, + 'IsInterlaced': false, + 'Channels': 6, + 'SampleRate': 48000, + 'IsDefault': true, + 'IsForced': false, + 'Type': 'Audio', + 'Index': stream.index, + 'IsExternal': false, + 'IsTextSubtitleStream': false, + 'SupportsExternalStream': false, + 'Level': 0 + }); + break; + case 'subtitle': + mediaStreams.push({ + 'Codec': stream.codec_name, + 'Language': stream.tags_language, + 'TimeBase': stream.time_base, + 'CodecTimeBase': stream.codec_time_base, + 'Title': stream.tags_title || stream.tags_language, + 'localizedUndefined': 'Undefined', + 'localizedDefault': 'Default', + 'localizedForced': 'Forced', + 'DisplayTitle': stream.tags_title || stream.tags_language, + 'IsInterlaced': false, + 'IsDefault': false, + 'IsForced': false, + 'Type': 'Subtitle', + 'Index': stream.index, + 'IsExternal': false, + 'IsTextSubtitleStream': true, + 'SupportsExternalStream': true, + 'Level': 0 + }); + break; + } + } + + return mediaStreams; +}; diff --git a/src/lib/embyEmulation/index.js b/src/lib/embyEmulation/index.js new file mode 100644 index 00000000..391362af --- /dev/null +++ b/src/lib/embyEmulation/index.js @@ -0,0 +1,116 @@ +import EmbyServerAPI from './ServerAPI'; + +import { v4 as uuidv4 } from 'uuid'; +import { User } from '../../models/user'; +import errors from 'restify-errors'; +import bcrypt from 'bcrypt'; +import Primus from 'primus'; +import { timeout } from 'async'; + +export default class EmbyEmulation { + /** + * + * @param {Oblecto} oblecto + */ + constructor(oblecto) { + this.oblecto = oblecto; + + this.sessions = {}; + + this.websocketSessions = {}; + + this.serverId = 'cadda85fd4f447b9ad3ccc3c83cf1cf6'; + this.version = '10.6.4'; + + this.serverName = 'Oblecto'; + + this.serverAPI = new EmbyServerAPI(this); + + this.primus = new Primus(this.serverAPI.server, { + pathname: '/socket', + authorization: function (req, done) { + + if (!req.query || !req.query.api_key) + return done({ statusCode: 403, message: '' }); + + this.auth = 'test'; + + done(); + } + }); + + this.primus.on('connection', (spark) => { + let req = spark.request; + + if (!req.query || !req.query.api_key) + return spark.disconnect(); + + this.websocketSessions[req.query.api_key] = spark; + + console.log('jellyfin ws client connected'); + + timeout(() => { + console.log('sending'); + spark.write({ + MessageType: 'Play', + Data: { + VolumeLevel: 100, + IsMuted: false, + IsPaused: false, + RepeatMode: 'RepeatNone', + ShuffleMode: 'Sorted', + MaxStreamingBitrate: 140000000, + PositionTicks: 0, + PlaybackStartTimeTicks: 15999190139560000, + SubtitleStreamIndex: 2, + AudioStreamIndex: 1, + BufferedRanges: [], + PlayMethod: 'DirectStream', + PlaySessionId: 'Thisisafuckingtest', + PlaylistItemId: 'playlistItem1', + MediaSourceId: 2725, + CanSeek: true, + ItemId: 'movie16', + NowPlayingQueue: [{ Id: 'movie16', PlaylistItemId: 'playlistItem1' }] + } + }); + }, 2000); + + spark.on('data', function message(data) { + + console.log('jellyfin ws recevied:', data); + }); + }); + } + + async handleLogin(username, password) { + let user = await User.findOne({ + where: { username: username }, + attributes: ['username', 'name', 'email', 'password', 'id'] + }); + + if (!user) throw Error('Incorrect username'); + + if (!await bcrypt.compare(password, user.password)) + throw Error('Password incorrect'); + + let HasPassword = user.password !== ''; + + let sessionId = uuidv4(); + + this.sessions[sessionId] = { + Name: user.name, + ServerId: this.serverId, + Id: user.id, + HasPassword, + HasConfiguredPassword: HasPassword, + HasConfiguredEasyPassword: false, + EnableAutoLogin: false, + LastLoginDate: '2020-09-11T23:37:27.3042432Z', + LastActivityDate: '2020-09-11T23:37:27.3042432Z', + capabilities: {} + }; + + return sessionId; + } +} diff --git a/src/lib/indexers/series/SeriesIndexer.js b/src/lib/indexers/series/SeriesIndexer.js index 386f1549..b19f078a 100644 --- a/src/lib/indexers/series/SeriesIndexer.js +++ b/src/lib/indexers/series/SeriesIndexer.js @@ -14,6 +14,7 @@ import { File } from '../../../models/file'; import IdentificationError from '../../errors/IdentificationError'; import logger from '../../../submodules/logger'; import guessit from '../../../submodules/guessit'; +import path from 'path'; /** * @typedef {import('../../oblecto').default} Oblecto @@ -63,19 +64,10 @@ export default class SeriesIndexer { * * @param {File} file - File to be indexed * @param {GuessitIdentification} guessitIdentification - Guessit identification Object + * @param seriesIdentification * @returns {Promise} - Matched series */ - async indexSeries(file, guessitIdentification) { - let seriesIdentification; - - try { - seriesIdentification = await this.seriesIdentifier.identify(file.path, guessitIdentification); - } catch (e) { - throw new IdentificationError(`Could not identify series of ${file.path}`); - } - - logger.log('DEBUG', `${file.path} series identified: ${seriesIdentification.seriesName}`); - + async indexSeries(seriesIdentification) { const identifiers = ['tvdbid', 'tmdbid']; let seriesQuery = []; @@ -100,11 +92,40 @@ export default class SeriesIndexer { } async identify(episodePath) { - const guessitIdentification = await guessit.identify(episodePath); - const seriesIdentification = await this.seriesIdentifier.identify(episodePath, guessitIdentification); + const identificationNames = [path.basename(episodePath), episodePath]; + + let guessitIdentification; + let seriesIdentification; + + let seriesIdentified = false; + + for (const name of identificationNames) { + try { + guessitIdentification = await guessit.identify(name); + + // Some single season shows usually don't have a season in the title, + // therefore whe should set it to 1 by default. + if (!guessitIdentification.season) { + guessitIdentification.season = 1; + } + + seriesIdentification = await this.seriesIdentifier.identify(name, guessitIdentification); + + seriesIdentified = true; + + break; + } catch (e) { + logger.log('DEBUG', 'Using for path for identifying', episodePath); + } + } + + if (seriesIdentified === false) { + throw new IdentificationError('Could not identify series'); + } + const episodeIdentification = await this.episodeIdentifer.identify(episodePath, guessitIdentification, seriesIdentification); - return { ...seriesIdentification, ...episodeIdentification }; + return { series: seriesIdentification, episode: episodeIdentification }; } /** @@ -116,26 +137,9 @@ export default class SeriesIndexer { async indexFile(episodePath) { let file = await this.oblecto.fileIndexer.indexVideoFile(episodePath); - /** - * @type {GuessitIdentification} - */ - const guessitIdentification = await guessit.identify(episodePath); + let { series: seriesIdentification, episode: episodeIdentification } = await this.identify(episodePath); - // Some single season shows usually don't have a season in the title, - // therefore whe should set it to 1 by default. - if (!guessitIdentification.season) { - guessitIdentification.season = 1; - } - - let series = await this.indexSeries(file, guessitIdentification); - - let episodeIdentification; - - try { - episodeIdentification = await this.episodeIdentifer.identify(episodePath, guessitIdentification, series); - } catch (e) { - throw new IdentificationError(`Could not identify episode ${episodePath}`); - } + const series = await this.indexSeries(seriesIdentification); logger.log('DEBUG', `${file.path} episode identified ${episodeIdentification.episodeName}`); diff --git a/src/lib/oblecto/index.js b/src/lib/oblecto/index.js index ba5468be..5e785667 100644 --- a/src/lib/oblecto/index.js +++ b/src/lib/oblecto/index.js @@ -43,6 +43,8 @@ import FileIndexer from '../indexers/files/FileIndexer'; import { initDatabase } from '../../submodules/database'; import StreamSessionController from '../streamSessions/StreamSessionController'; import SeedboxController from '../seedbox/SeedboxController'; +import {initDatabes} from '../../submodules/database'; +import EmbyEmulation from '../embyEmulation'; export default class Oblecto { /** @@ -104,6 +106,9 @@ export default class Oblecto { this.oblectoAPI = new OblectoAPI(this); this.realTimeController = new RealtimeController(this); + + // Emby Server emulation + this.embyServer = new EmbyEmulation(this); } close() { diff --git a/src/lib/seedbox/Seedbox.js b/src/lib/seedbox/Seedbox.js index a9b224e4..73e80027 100644 --- a/src/lib/seedbox/Seedbox.js +++ b/src/lib/seedbox/Seedbox.js @@ -2,7 +2,7 @@ import { extname, normalize as normalizePath } from 'path'; import SeedboxImportFTP from './SeedboxImportDrivers/SeedboxImportFTP'; import WarnExtendableError from '../errors/WarnExtendableError'; import logger from '../../submodules/logger'; -import index from 'async'; +import SeedboxImportFTPS from './SeedboxImportDrivers/SeedboxImportFTPS'; export default class Seedbox { constructor(seedboxConfig) { @@ -18,11 +18,18 @@ export default class Seedbox { case 'ftp': this.storageDriver = new SeedboxImportFTP(seedboxStorageDriverOptions); return; + case 'ftps': + this.storageDriver = new SeedboxImportFTPS(seedboxStorageDriverOptions); + return; } return new WarnExtendableError('Invalid seedbox storage driver'); } + async setupDriver() { + await this.storageDriver.setup(); + } + async findAll(indexPath, fileTypes) { logger.log('DEBUG', `Finding files in ${indexPath}`); diff --git a/src/lib/seedbox/SeedboxController.js b/src/lib/seedbox/SeedboxController.js index 71e9008b..5df848b1 100644 --- a/src/lib/seedbox/SeedboxController.js +++ b/src/lib/seedbox/SeedboxController.js @@ -5,6 +5,7 @@ import { Movie } from '../../models/movie'; import { File } from '../../models/file'; import Queue from '../queue'; import { basename, parse, dirname } from 'path'; +import path from 'path'; import { rename } from 'fs/promises'; import mkdirp from 'mkdirp'; @@ -22,7 +23,7 @@ export default class SeedboxController { this.oblecto = oblecto; this.seedBoxes = []; - this.importQueue = new Queue(1); + this.importQueue = new Queue(oblecto.config.seedboxImport.concurrency); this.importQueue.registerJob('importMovie', job => this.importMovie(job.seedbox, job.origin, job.destination)); this.importQueue.registerJob('importEpisode', job => this.importEpisode(job.seedbox, job.origin, job.destination)); @@ -33,7 +34,7 @@ export default class SeedboxController { for (const seedbox of this.oblecto.config.seedboxes) { if (!seedbox.enabled) continue; - this.addSeedbox(seedbox); + await this.addSeedbox(seedbox); } await this.importAllEpisodes(); @@ -47,8 +48,12 @@ export default class SeedboxController { ); } - addSeedbox(seedboxConfig) { - this.seedBoxes.push(new Seedbox(seedboxConfig)); + async addSeedbox(seedboxConfig) { + const newSeedbox = new Seedbox(seedboxConfig); + + await newSeedbox.setupDriver(); + + this.seedBoxes.push(newSeedbox); logger.log('DEBUG', `Loaded seedbox ${seedboxConfig.name}`); } @@ -139,7 +144,7 @@ export default class SeedboxController { this.importQueue.pushJob('importMovie', { seedbox, origin: file, - destination: this.oblecto.config.movies.directories[0].path + '/' + basename(file) + destination: path.join(this.oblecto.config.movies.directories[0].path, basename(file)) }); } } @@ -173,7 +178,7 @@ export default class SeedboxController { this.importQueue.pushJob('importEpisode', { seedbox, origin: file, - destination: this.oblecto.config.tvshows.directories[0].path + '/' + identification.seriesName + '/' + basename(file) + destination: path.join(this.oblecto.config.tvshows.directories[0].path, identification.series.seriesName, basename(file)) }); } } diff --git a/src/lib/seedbox/SeedboxImportDriver.js b/src/lib/seedbox/SeedboxImportDriver.js index 76db0ea4..c4e7b252 100644 --- a/src/lib/seedbox/SeedboxImportDriver.js +++ b/src/lib/seedbox/SeedboxImportDriver.js @@ -1,5 +1,5 @@ export default class SeedboxImportDriver { constructor(seedboxStorageDriverOptions) { - + this.config = seedboxStorageDriverOptions; } } diff --git a/src/lib/seedbox/SeedboxImportDrivers/SeedboxImportFTPS.js b/src/lib/seedbox/SeedboxImportDrivers/SeedboxImportFTPS.js new file mode 100644 index 00000000..54e122e9 --- /dev/null +++ b/src/lib/seedbox/SeedboxImportDrivers/SeedboxImportFTPS.js @@ -0,0 +1,55 @@ +import SeedboxImportDriver from '../SeedboxImportDriver'; +import logger from '../../../submodules/logger'; + +import * as ftp from 'basic-ftp'; + +export default class SeedboxImportFTPS extends SeedboxImportDriver { + constructor(config) { + super(config); + + this.client = new ftp.Client(); + + this.client.ftp.verbose = false; + } + + async setup() { + try { + + console.log(this.config); + + await this.client.access({ + host: this.config.host, + user: this.config.username, + password: this.config.password, + secure: this.config.secure || false + }); + + } catch (e) { + logger.log('INFO', e); + } + } + + async list(path) { + const listing = await this.client.list(path); + + return listing.map(item => { + return { + name: item.name, + type: item.type - 1 + }; + }); + } + + async copy(origin, destination) { + const client = new ftp.Client(); + + await client.access({ + host: this.config.host, + user: this.config.username, + password: this.config.password, + secure: this.config.secure || false + }); + + return await client.downloadTo(destination, origin); + } +} diff --git a/src/lib/seedbox/SeedboxImporter.js b/src/lib/seedbox/SeedboxImporter.js deleted file mode 100644 index e69de29b..00000000 diff --git a/src/lib/updaters/movies/informationRetrievers/TmdbMovieRetriever.js b/src/lib/updaters/movies/informationRetrievers/TmdbMovieRetriever.js index 80ecfa6b..8d3bd125 100644 --- a/src/lib/updaters/movies/informationRetrievers/TmdbMovieRetriever.js +++ b/src/lib/updaters/movies/informationRetrievers/TmdbMovieRetriever.js @@ -22,7 +22,7 @@ export default class TmdbMovieRetriever { * @returns {Promise<{originalName: string, overview: *, revenue: *, releaseDate: string, imdbid: string, genres: string, popularity: *, tagline: *, runtime: *, originalLanguage: string, movieName: *, budget: *}>} - Movie metadata */ async retrieveInformation(movie) { - let movieInfo = await promiseTimeout(this.oblecto.tmdb.movieInfo({ id: movie.tmdbid }, { timeout: 5000 })); + let movieInfo = await this.oblecto.tmdb.movieInfo({ id: movie.tmdbid }); let data = { imdbid: movieInfo.imdb_id, diff --git a/src/submodules/logger/index.js b/src/submodules/logger/index.js index 8ef1d4a4..37d87d9a 100644 --- a/src/submodules/logger/index.js +++ b/src/submodules/logger/index.js @@ -11,7 +11,7 @@ class Logger extends EventEmitter{ this.on('log', (log) => { if(this.silent) return; if (log instanceof FileExistsError) return; - if (log.level === 'DEBUG') return; + // if (log.level === 'DEBUG') return; if (log instanceof ExtendableError) { console.log(log.level, log); diff --git a/tests/mocha/SeriesIndexer.spec.js b/tests/mocha/SeriesIndexer.spec.js index ab0d95b8..cf809a69 100644 --- a/tests/mocha/SeriesIndexer.spec.js +++ b/tests/mocha/SeriesIndexer.spec.js @@ -44,6 +44,13 @@ describe('SeriesIndexer', function () { expect(identification.seriesName).to.be('The Flash (2014)'); expect(identification.tvdbid).to.be(279121); }); + + it('/mnt/Media/Series/Catch-22/Catch-22.S01E06.2160p.HULU.WEB-DL.DDP5.1.DV.H.265-NTb.mkv', async function () { + const seriesIndexer = new SeriesIndexer(oblecto); + const identification = await seriesIndexer.seriesIdentifier.identify('/mnt/Media/Series/Catch-22/Catch-22.S01E06.2160p.HULU.WEB-DL.DDP5.1.DV.H.265-NTb.mkv', await guessit.identify('/mnt/Media/Series/Catch-22/Catch-22.S01E06.2160p.HULU.WEB-DL.DDP5.1.DV.H.265-NTb.mkv')); + + console.log(identification); + }); }); describe('Aggregate Episode Identifier', async function () { @@ -81,5 +88,15 @@ describe('SeriesIndexer', function () { expect(identification.tvdbid).to.be(6885898); expect(identification.imdbid).to.be('tt8312898'); }); + it('/mnt/Media/Series/Catch-22/Catch-22.S01E06.2160p.HULU.WEB-DL.DDP5.1.DV.H.265-NTb.mkv', async function () { + const seriesIndexer = new SeriesIndexer(oblecto); + const path = '/mnt/Media/Series/Catch-22/Catch-22.S01E06.2160p.HULU.WEB-DL.DDP5.1.DV.H.265-NTb.mkv'; + + const identification = await seriesIndexer.identify(path); + + console.log(identification); + + }); }); + });