Add:User listening sessions page, Update:Listening sessions to save media times and device info

This commit is contained in:
advplyr 2022-05-26 19:09:46 -05:00
parent 54663f0f01
commit f002532c1e
12 changed files with 466 additions and 20 deletions

View file

@ -189,8 +189,8 @@ class LibraryItemController {
Logger.error(`[LibraryItemController] startPlaybackSession cannot playback ${req.libraryItem.id}`)
return res.sendStatus(404)
}
const options = req.body || {}
this.playbackSessionManager.startSessionRequest(req.user, req.libraryItem, null, options, res)
this.playbackSessionManager.startSessionRequest(req, res, null)
}
// POST: api/items/:id/play/:episodeId
@ -206,8 +206,7 @@ class LibraryItemController {
return res.sendStatus(404)
}
const options = req.body || {}
this.playbackSessionManager.startSessionRequest(req.user, libraryItem, episodeId, options, res)
this.playbackSessionManager.startSessionRequest(req, res, episodeId)
}
// PATCH: api/items/:id/tracks

5
server/libs/isJs.js Normal file

File diff suppressed because one or more lines are too long

174
server/libs/requestIp.js Normal file
View file

@ -0,0 +1,174 @@
// SOURCE: https://github.com/pbojinov/request-ip
"use strict";
function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
var is = require('./isJs');
/**
* Parse x-forwarded-for headers.
*
* @param {string} value - The value to be parsed.
* @return {string|null} First known IP address, if any.
*/
function getClientIpFromXForwardedFor(value) {
if (!is.existy(value)) {
return null;
}
if (is.not.string(value)) {
throw new TypeError("Expected a string, got \"".concat(_typeof(value), "\""));
} // x-forwarded-for may return multiple IP addresses in the format:
// "client IP, proxy 1 IP, proxy 2 IP"
// Therefore, the right-most IP address is the IP address of the most recent proxy
// and the left-most IP address is the IP address of the originating client.
// source: http://docs.aws.amazon.com/elasticloadbalancing/latest/classic/x-forwarded-headers.html
// Azure Web App's also adds a port for some reason, so we'll only use the first part (the IP)
var forwardedIps = value.split(',').map(function (e) {
var ip = e.trim();
if (ip.includes(':')) {
var splitted = ip.split(':'); // make sure we only use this if it's ipv4 (ip:port)
if (splitted.length === 2) {
return splitted[0];
}
}
return ip;
}); // Sometimes IP addresses in this header can be 'unknown' (http://stackoverflow.com/a/11285650).
// Therefore taking the left-most IP address that is not unknown
// A Squid configuration directive can also set the value to "unknown" (http://www.squid-cache.org/Doc/config/forwarded_for/)
return forwardedIps.find(is.ip);
}
/**
* Determine client IP address.
*
* @param req
* @returns {string} ip - The IP address if known, defaulting to empty string if unknown.
*/
function getClientIp(req) {
// Server is probably behind a proxy.
if (req.headers) {
// Standard headers used by Amazon EC2, Heroku, and others.
if (is.ip(req.headers['x-client-ip'])) {
return req.headers['x-client-ip'];
} // Load-balancers (AWS ELB) or proxies.
var xForwardedFor = getClientIpFromXForwardedFor(req.headers['x-forwarded-for']);
if (is.ip(xForwardedFor)) {
return xForwardedFor;
} // Cloudflare.
// @see https://support.cloudflare.com/hc/en-us/articles/200170986-How-does-Cloudflare-handle-HTTP-Request-headers-
// CF-Connecting-IP - applied to every request to the origin.
if (is.ip(req.headers['cf-connecting-ip'])) {
return req.headers['cf-connecting-ip'];
} // Fastly and Firebase hosting header (When forwared to cloud function)
if (is.ip(req.headers['fastly-client-ip'])) {
return req.headers['fastly-client-ip'];
} // Akamai and Cloudflare: True-Client-IP.
if (is.ip(req.headers['true-client-ip'])) {
return req.headers['true-client-ip'];
} // Default nginx proxy/fcgi; alternative to x-forwarded-for, used by some proxies.
if (is.ip(req.headers['x-real-ip'])) {
return req.headers['x-real-ip'];
} // (Rackspace LB and Riverbed's Stingray)
// http://www.rackspace.com/knowledge_center/article/controlling-access-to-linux-cloud-sites-based-on-the-client-ip-address
// https://splash.riverbed.com/docs/DOC-1926
if (is.ip(req.headers['x-cluster-client-ip'])) {
return req.headers['x-cluster-client-ip'];
}
if (is.ip(req.headers['x-forwarded'])) {
return req.headers['x-forwarded'];
}
if (is.ip(req.headers['forwarded-for'])) {
return req.headers['forwarded-for'];
}
if (is.ip(req.headers.forwarded)) {
return req.headers.forwarded;
}
} // Remote address checks.
if (is.existy(req.connection)) {
if (is.ip(req.connection.remoteAddress)) {
return req.connection.remoteAddress;
}
if (is.existy(req.connection.socket) && is.ip(req.connection.socket.remoteAddress)) {
return req.connection.socket.remoteAddress;
}
}
if (is.existy(req.socket) && is.ip(req.socket.remoteAddress)) {
return req.socket.remoteAddress;
}
if (is.existy(req.info) && is.ip(req.info.remoteAddress)) {
return req.info.remoteAddress;
} // AWS Api Gateway + Lambda
if (is.existy(req.requestContext) && is.existy(req.requestContext.identity) && is.ip(req.requestContext.identity.sourceIp)) {
return req.requestContext.identity.sourceIp;
}
return null;
}
/**
* Expose request IP as a middleware.
*
* @param {object} [options] - Configuration.
* @param {string} [options.attributeName] - Name of attribute to augment request object with.
* @return {*}
*/
function mw(options) {
// Defaults.
var configuration = is.not.existy(options) ? {} : options; // Validation.
if (is.not.object(configuration)) {
throw new TypeError('Options must be an object!');
}
var attributeName = configuration.attributeName || 'clientIp';
return function (req, res, next) {
var ip = getClientIp(req);
Object.defineProperty(req, attributeName, {
get: function get() {
return ip;
},
configurable: true
});
next();
};
}
module.exports = {
getClientIpFromXForwardedFor: getClientIpFromXForwardedFor,
getClientIp: getClientIp,
mw: mw
};

File diff suppressed because one or more lines are too long

View file

@ -1,11 +1,16 @@
const Path = require('path')
const date = require('date-and-time')
const serverVersion = require('../../package.json').version
const { PlayMethod } = require('../utils/constants')
const PlaybackSession = require('../objects/PlaybackSession')
const DeviceInfo = require('../objects/DeviceInfo')
const Stream = require('../objects/Stream')
const Logger = require('../Logger')
const fs = require('fs-extra')
const uaParserJs = require('../libs/uaParserJs')
const requestIp = require('../libs/requestIp')
class PlaybackSessionManager {
constructor(db, emitter, clientEmitter) {
this.db = db
@ -27,8 +32,21 @@ class PlaybackSessionManager {
return session ? session.stream : null
}
async startSessionRequest(user, libraryItem, episodeId, options, res) {
const session = await this.startSession(user, libraryItem, episodeId, options)
getDeviceInfo(req) {
const ua = uaParserJs(req.headers['user-agent'])
const ip = requestIp.getClientIp(req)
const clientDeviceInfo = req.body ? req.body.deviceInfo || null : null // From mobile client
const deviceInfo = new DeviceInfo()
deviceInfo.setData(ip, ua, clientDeviceInfo, serverVersion)
return deviceInfo
}
async startSessionRequest(req, res, episodeId) {
const deviceInfo = this.getDeviceInfo(req)
const { user, libraryItem, body: options } = req
const session = await this.startSession(user, deviceInfo, libraryItem, episodeId, options)
res.json(session.toJSONForClient(libraryItem))
}
@ -84,7 +102,7 @@ class PlaybackSessionManager {
res.sendStatus(200)
}
async startSession(user, libraryItem, episodeId, options) {
async startSession(user, deviceInfo, libraryItem, episodeId, options) {
// Close any sessions already open for user
var userSessions = this.sessions.filter(playbackSession => playbackSession.userId === user.id)
for (const session of userSessions) {
@ -99,7 +117,7 @@ class PlaybackSessionManager {
var userStartTime = 0
if (userProgress) userStartTime = Number.parseFloat(userProgress.currentTime) || 0
const newPlaybackSession = new PlaybackSession()
newPlaybackSession.setData(libraryItem, user, mediaPlayer, episodeId)
newPlaybackSession.setData(libraryItem, user, mediaPlayer, deviceInfo, userStartTime, episodeId)
var audioTracks = []
if (shouldDirectPlay) {
@ -122,7 +140,6 @@ class PlaybackSessionManager {
})
}
newPlaybackSession.currentTime = userStartTime
newPlaybackSession.audioTracks = audioTracks
// Will save on the first sync

View file

@ -0,0 +1,74 @@
class DeviceInfo {
constructor(deviceInfo = null) {
this.ipAddress = null
// From User Agent (see: https://www.npmjs.com/package/ua-parser-js)
this.browserName = null
this.browserVersion = null
this.osName = null
this.osVersion = null
this.deviceType = null
// From client
this.clientVersion = null
this.manufacturer = null
this.model = null
this.sdkVersion = null // Android Only
this.serverVersion = null
if (deviceInfo) {
this.construct(deviceInfo)
}
}
construct(deviceInfo) {
for (const key in deviceInfo) {
if (deviceInfo[key] !== undefined && this[key] !== undefined) {
this[key] = deviceInfo[key]
}
}
}
toJSON() {
const obj = {
ipAddress: this.ipAddress,
browserName: this.browserName,
browserVersion: this.browserVersion,
osName: this.osName,
osVersion: this.osVersion,
deviceType: this.deviceType,
clientVersion: this.clientVersion,
manufacturer: this.manufacturer,
model: this.model,
sdkVersion: this.sdkVersion,
serverVersion: this.serverVersion
}
for (const key in obj) {
if (obj[key] === null || obj[key] === undefined) {
delete obj[key]
}
}
return obj
}
setData(ip, ua, clientDeviceInfo, serverVersion) {
this.ipAddress = ip || null
const uaObj = ua || {}
this.browserName = uaObj.browser.name || null
this.browserVersion = uaObj.browser.version || null
this.osName = uaObj.os.name || null
this.osVersion = uaObj.os.version || null
this.deviceType = uaObj.device.type || null
var cdi = clientDeviceInfo || {}
this.clientVersion = cdi.clientVersion || null
this.manufacturer = cdi.manufacturer || null
this.model = cdi.model || null
this.sdkVersion = cdi.sdkVersion || null
this.serverVersion = serverVersion || null
}
}
module.exports = DeviceInfo

View file

@ -3,6 +3,7 @@ const { getId } = require('../utils/index')
const { PlayMethod } = require('../utils/constants')
const BookMetadata = require('./metadata/BookMetadata')
const PodcastMetadata = require('./metadata/PodcastMetadata')
const DeviceInfo = require('./DeviceInfo')
class PlaybackSession {
constructor(session) {
@ -21,18 +22,21 @@ class PlaybackSession {
this.playMethod = null
this.mediaPlayer = null
this.deviceInfo = null
this.date = null
this.dayOfWeek = null
this.timeListening = null
this.startTime = null // media current time at start of playback
this.currentTime = 0 // Last current time set
this.startedAt = null
this.updatedAt = null
// Not saved in DB
this.lastSave = 0
this.audioTracks = []
this.currentTime = 0
this.stream = null
if (session) {
@ -56,10 +60,13 @@ class PlaybackSession {
duration: this.duration,
playMethod: this.playMethod,
mediaPlayer: this.mediaPlayer,
deviceInfo: this.deviceInfo ? this.deviceInfo.toJSON() : null,
date: this.date,
dayOfWeek: this.dayOfWeek,
timeListening: this.timeListening,
lastUpdate: this.lastUpdate,
startTime: this.startTime,
currentTime: this.currentTime,
startedAt: this.startedAt,
updatedAt: this.updatedAt
}
}
@ -80,13 +87,15 @@ class PlaybackSession {
duration: this.duration,
playMethod: this.playMethod,
mediaPlayer: this.mediaPlayer,
deviceInfo: this.deviceInfo ? this.deviceInfo.toJSON() : null,
date: this.date,
dayOfWeek: this.dayOfWeek,
timeListening: this.timeListening,
lastUpdate: this.lastUpdate,
startTime: this.startTime,
currentTime: this.currentTime,
startedAt: this.startedAt,
updatedAt: this.updatedAt,
audioTracks: this.audioTracks.map(at => at.toJSON()),
currentTime: this.currentTime,
libraryItem: libraryItem.toJSONExpanded()
}
}
@ -101,6 +110,7 @@ class PlaybackSession {
this.duration = session.duration
this.playMethod = session.playMethod
this.mediaPlayer = session.mediaPlayer || null
this.deviceInfo = new DeviceInfo(session.deviceInfo)
this.chapters = session.chapters || []
this.mediaMetadata = null
@ -118,6 +128,9 @@ class PlaybackSession {
this.dayOfWeek = session.dayOfWeek
this.timeListening = session.timeListening || null
this.startTime = session.startTime || 0
this.currentTime = session.currentTime || 0
this.startedAt = session.startedAt
this.updatedAt = session.updatedAt || null
}
@ -127,7 +140,7 @@ class PlaybackSession {
return Math.max(0, Math.min(this.currentTime / this.duration, 1))
}
setData(libraryItem, user, mediaPlayer, episodeId = null) {
setData(libraryItem, user, mediaPlayer, deviceInfo, startTime, episodeId = null) {
this.id = getId('play')
this.userId = user.id
this.libraryItemId = libraryItem.id
@ -146,8 +159,13 @@ class PlaybackSession {
}
this.mediaPlayer = mediaPlayer
this.deviceInfo = deviceInfo || new DeviceInfo()
this.timeListening = 0
this.startTime = startTime
this.currentTime = startTime
this.date = date.format(new Date(), 'YYYY-MM-DD')
this.dayOfWeek = date.format(new Date(), 'dddd')
this.startedAt = Date.now()