mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-12-27 06:09:38 +00:00
Add:User listening sessions page, Update:Listening sessions to save media times and device info
This commit is contained in:
parent
54663f0f01
commit
f002532c1e
12 changed files with 466 additions and 20 deletions
|
|
@ -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
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
174
server/libs/requestIp.js
Normal 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
|
||||
};
|
||||
4
server/libs/uaParserJs.js
Normal file
4
server/libs/uaParserJs.js
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -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
|
||||
|
|
|
|||
74
server/objects/DeviceInfo.js
Normal file
74
server/objects/DeviceInfo.js
Normal 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
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue