mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-01-22 00:07:52 +01: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
@ -32,6 +32,7 @@ export default {
|
||||
default: ''
|
||||
},
|
||||
paddingX: Number,
|
||||
paddingY: Number,
|
||||
small: Boolean,
|
||||
loading: Boolean,
|
||||
disabled: Boolean
|
||||
@ -48,14 +49,17 @@ export default {
|
||||
if (this.small) {
|
||||
list.push('text-sm')
|
||||
if (this.paddingX === undefined) list.push('px-4')
|
||||
list.push('py-1')
|
||||
if (this.paddingY === undefined) list.push('py-1')
|
||||
} else {
|
||||
if (this.paddingX === undefined) list.push('px-8')
|
||||
list.push('py-2')
|
||||
if (this.paddingY === undefined) list.push('py-2')
|
||||
}
|
||||
if (this.paddingX !== undefined) {
|
||||
list.push(`px-${this.paddingX}`)
|
||||
}
|
||||
if (this.paddingY !== undefined) {
|
||||
list.push(`py-${this.paddingY}`)
|
||||
}
|
||||
if (this.disabled) {
|
||||
list.push('cursor-not-allowed')
|
||||
}
|
||||
|
@ -22,7 +22,10 @@
|
||||
<div class="w-full h-px bg-white bg-opacity-10 my-2" />
|
||||
<div class="py-2">
|
||||
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Listening Stats</h1>
|
||||
<p class="text-sm text-gray-300">{{ listeningSessions.length }} Listening Sessions</p>
|
||||
<div class="flex items-center">
|
||||
<p class="text-sm text-gray-300">{{ listeningSessions.length }} Listening Sessions</p>
|
||||
<ui-btn :to="`/config/users/${user.id}/sessions`" class="text-xs mx-2" :padding-x="1.5" :padding-y="1">View All</ui-btn>
|
||||
</div>
|
||||
<p class="text-sm text-gray-300">
|
||||
Total Time Listened:
|
||||
<span class="font-mono text-base">{{ listeningTimePretty }}</span>
|
||||
@ -35,7 +38,7 @@
|
||||
<div v-if="latestSession" class="mt-4">
|
||||
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Last Listening Session</h1>
|
||||
<p class="text-sm text-gray-300">
|
||||
<strong>{{ latestSession.displayTitle }}</strong> {{ $dateDistanceFromNow(latestSession.updatedAt) }} for <span class="font-mono text-base">{{ $elapsedPrettyExtended(this.latestSession.timeListening) }}</span>
|
||||
<strong>{{ latestSession.displayTitle }}</strong> {{ $dateDistanceFromNow(latestSession.updatedAt) }} for <span class="font-mono text-base">{{ $elapsedPrettyExtended(this.latestSession.timeListening) }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -73,7 +76,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p v-else class="text-white text-opacity-50">Nothing read yet...</p>
|
||||
<p v-else class="text-white text-opacity-50">Nothing listened to yet...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
146
client/pages/config/users/_id/sessions.vue
Normal file
146
client/pages/config/users/_id/sessions.vue
Normal file
@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<div class="w-full h-full">
|
||||
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-0 sm:p-4 mb-8">
|
||||
<nuxt-link :to="`/config/users/${user.id}`" class="text-white text-opacity-70 hover:text-opacity-100 hover:bg-white hover:bg-opacity-5 cursor-pointer rounded-full px-2 sm:px-0">
|
||||
<div class="flex items-center">
|
||||
<div class="h-10 w-10 flex items-center justify-center">
|
||||
<span class="material-icons text-2xl">arrow_back</span>
|
||||
</div>
|
||||
<p class="pl-1">Back to User</p>
|
||||
</div>
|
||||
</nuxt-link>
|
||||
<div class="flex items-center mb-2 mt-4 px-2 sm:px-0">
|
||||
<widgets-online-indicator :value="!!userOnline" />
|
||||
<h1 class="text-xl pl-2">{{ username }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="w-full h-px bg-white bg-opacity-10 my-2" />
|
||||
|
||||
<div class="py-2">
|
||||
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Listening Sessions</h1>
|
||||
<table v-if="listeningSessions.length" class="userSessionsTable">
|
||||
<tr class="bg-primary bg-opacity-40">
|
||||
<th class="flex-grow text-left">Item</th>
|
||||
<th class="w-40 text-left hidden md:table-cell">Play Method</th>
|
||||
<th class="w-40 text-left hidden sm:table-cell">Device Info</th>
|
||||
<th class="w-20">Listening Time</th>
|
||||
<th class="w-20">Last Time</th>
|
||||
<!-- <th class="w-40 hidden sm:table-cell">Started At</th> -->
|
||||
<th class="w-40 hidden sm:table-cell">Last Update</th>
|
||||
</tr>
|
||||
<tr v-for="session in listeningSessions" :key="session.id">
|
||||
<td class="py-1">
|
||||
<p class="text-sm text-gray-200">{{ session.displayTitle }}</p>
|
||||
<p class="text-xs text-gray-400">{{ session.displayAuthor }}</p>
|
||||
</td>
|
||||
<td class="hidden md:table-cell">
|
||||
<p class="text-xs">{{ getPlayMethodName(session.playMethod) }} with {{ session.mediaPlayer }}</p>
|
||||
</td>
|
||||
<td class="hidden sm:table-cell">
|
||||
<p class="text-xs" v-html="getDeviceInfoString(session.deviceInfo)" />
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p>
|
||||
</td>
|
||||
<!-- <td class="text-center hidden sm:table-cell">
|
||||
<ui-tooltip v-if="session.startedAt" direction="top" :text="$formatDate(session.startedAt, 'MMMM do, yyyy HH:mm')">
|
||||
<p class="text-xs">{{ $dateDistanceFromNow(session.startedAt) }}</p>
|
||||
</ui-tooltip>
|
||||
</td> -->
|
||||
<td class="text-center hidden sm:table-cell">
|
||||
<ui-tooltip v-if="session.updatedAt" direction="top" :text="$formatDate(session.updatedAt, 'MMMM do, yyyy HH:mm')">
|
||||
<p class="text-xs">{{ $dateDistanceFromNow(session.updatedAt) }}</p>
|
||||
</ui-tooltip>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p v-else class="text-white text-opacity-50">No sessions yet...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
async asyncData({ params, redirect, app }) {
|
||||
var user = await app.$axios.$get(`/api/users/${params.id}`).catch((error) => {
|
||||
console.error('Failed to get user', error)
|
||||
return null
|
||||
})
|
||||
if (!user) return redirect('/config/users')
|
||||
return {
|
||||
user
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
listeningSessions: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
username() {
|
||||
return this.user.username
|
||||
},
|
||||
userOnline() {
|
||||
return this.$store.getters['users/getIsUserOnline'](this.user.id)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getDeviceInfoString(deviceInfo) {
|
||||
if (!deviceInfo) return ''
|
||||
var lines = []
|
||||
if (deviceInfo.osName) lines.push(`${deviceInfo.osName} ${deviceInfo.osVersion}`)
|
||||
if (deviceInfo.browserName) lines.push(deviceInfo.browserName)
|
||||
|
||||
if (deviceInfo.manufacturer && deviceInfo.model) lines.push(`${deviceInfo.manufacturer} ${deviceInfo.model}`)
|
||||
if (deviceInfo.sdkVersion) lines.push(`SDK Version: ${deviceInfo.sdkVersion}`)
|
||||
return lines.join('<br>')
|
||||
},
|
||||
getPlayMethodName(playMethod) {
|
||||
if (playMethod === this.$constants.PlayMethod.DIRECTPLAY) return 'Direct Play'
|
||||
else if (playMethod === this.$constants.PlayMethod.TRANSCODE) return 'Transcode'
|
||||
else if (playMethod === this.$constants.PlayMethod.DIRECTSTREAM) return 'Direct Stream'
|
||||
else if (playMethod === this.$constants.PlayMethod.LOCAL) return 'Local'
|
||||
return 'Unknown'
|
||||
},
|
||||
async init() {
|
||||
console.log(navigator)
|
||||
|
||||
this.listeningSessions = await this.$axios.$get(`/api/users/${this.user.id}/listening-sessions`).catch((err) => {
|
||||
console.error('Failed to load listening sesions', err)
|
||||
return []
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.init()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.userSessionsTable {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
border: 1px solid #474747;
|
||||
}
|
||||
.userSessionsTable tr:nth-child(even) {
|
||||
background-color: #2e2e2e;
|
||||
}
|
||||
.userSessionsTable tr:not(:first-child) {
|
||||
background-color: #373838;
|
||||
}
|
||||
.userSessionsTable tr:hover:not(:first-child) {
|
||||
background-color: #474747;
|
||||
}
|
||||
.userSessionsTable td {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
.userSessionsTable th {
|
||||
padding: 4px 8px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
</style>
|
@ -28,7 +28,8 @@ const BookshelfView = {
|
||||
const PlayMethod = {
|
||||
DIRECTPLAY: 0,
|
||||
DIRECTSTREAM: 1,
|
||||
TRANSCODE: 2
|
||||
TRANSCODE: 2,
|
||||
LOCAL: 3
|
||||
}
|
||||
|
||||
const Constants = {
|
||||
|
@ -57,6 +57,7 @@ Vue.prototype.$elapsedPretty = (seconds, useFullNames = false) => {
|
||||
}
|
||||
|
||||
Vue.prototype.$secondsToTimestamp = (seconds) => {
|
||||
if (!seconds) return '0:00'
|
||||
var _seconds = seconds
|
||||
var _minutes = Math.floor(seconds / 60)
|
||||
_seconds -= _minutes * 60
|
||||
|
@ -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…
Reference in New Issue
Block a user