From 1022033e0f0f7d596fa002cb147841aca1121b82 Mon Sep 17 00:00:00 2001 From: benonymity Date: Wed, 6 Jul 2022 16:55:58 -0400 Subject: [PATCH 01/10] feat: share latest activity --- client/components/app/SideRail.vue | 20 ++++ client/pages/account.vue | 20 ++++ client/pages/social.vue | 134 +++++++++++++++++++++++++++ server/controllers/MiscController.js | 22 +++++ server/objects/user/User.js | 3 +- server/routers/ApiRouter.js | 1 + 6 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 client/pages/social.vue diff --git a/client/components/app/SideRail.vue b/client/components/app/SideRail.vue index eddcdf26d..f67383bac 100644 --- a/client/components/app/SideRail.vue +++ b/client/components/app/SideRail.vue @@ -63,6 +63,14 @@
+ + connect_without_contact + +

Social

+ +
+ + warning @@ -128,6 +136,9 @@ export default { isAuthorsPage() { return this.$route.name === 'library-library-authors' }, + isSocialPage() { + return this.$route.name === 'social' + }, libraryBookshelfPage() { return this.$route.name === 'library-library-bookshelf-id' }, @@ -137,6 +148,15 @@ export default { filterBy() { return this.$store.getters['user/getUserSetting']('filterBy') }, + async socialTrue() { + let listeningStats = await this.$axios.$get(`/api/social`).catch((err) => { + console.error('Failed to load shared user listening sesions', err) + return false + }) + if (listeningStats != null) { + return true + } else {return false} + }, showingIssues() { if (!this.$route.query) return false return this.libraryBookshelfPage && this.$route.query.filter === 'issues' diff --git a/client/pages/account.vue b/client/pages/account.vue index 88c294c1c..cff547e50 100644 --- a/client/pages/account.vue +++ b/client/pages/account.vue @@ -15,6 +15,18 @@
+
+ + +

+ Share Listening Activity + info_outlined +

+
+
+ +
+

Change Password

@@ -63,6 +75,9 @@ export default { }, isGuest() { return this.usertype === 'guest' + }, + isSharingActivity() { + return this.$store.getters['user/getUserSetting']('shareListeningActivity') } }, methods: { @@ -111,6 +126,11 @@ export default { this.$toast.error('Api call failed') this.changingPassword = false }) + }, + changeSharingSetting() { + console.log('test') + console.log(String(!this.isSharingActivity)) + this.$store.dispatch('user/updateUserSettings', { shareListeningActivity: !this.isSharingActivity }) } }, mounted() {} diff --git a/client/pages/social.vue b/client/pages/social.vue new file mode 100644 index 000000000..9baf91a72 --- /dev/null +++ b/client/pages/social.vue @@ -0,0 +1,134 @@ + + + diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index 399ce7428..f8ed93f03 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -263,5 +263,27 @@ class MiscController { }) res.json(tags) } + // GET: api/social + async getSocialStats(req, res) { + var filteredUsers = this.db.users.filter(c => c.settings.shareListeningActivity == true) + var userData = [] + for (let i = 0; i < filteredUsers.length; i++) { + var user = filteredUsers[i] + var listeningStats = await this.getUserListeningStatsHelpers(user.id) + var latestItem = user.mediaProgress.sort(p => p.lastUpdate)[0] + var totalItems = Object.keys(listeningStats.items).length + var totalDays = Object.keys(listeningStats.days).length + var session = { + username: user.username, + lastSeen: user.lastSeen, + latest: latestItem || null, + itemsRead: totalItems || 0, + minutesListened: listeningStats.totalTime || 0, + daysListened: totalDays| 0, + } + userData.push(session) + } + res.json(userData) + } } module.exports = new MiscController() \ No newline at end of file diff --git a/server/objects/user/User.js b/server/objects/user/User.js index c33fb3d0e..dd33c4b45 100644 --- a/server/objects/user/User.js +++ b/server/objects/user/User.js @@ -68,7 +68,8 @@ class User { filterBy: 'all', playbackRate: 1, bookshelfCoverSize: 120, - collapseSeries: false + collapseSeries: false, + shareListeningActivity: false } } diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 1fc5b2c0e..e093410ed 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -209,6 +209,7 @@ class ApiRouter { this.router.get('/search/podcast', MiscController.findPodcasts.bind(this)) this.router.get('/search/authors', MiscController.findAuthor.bind(this)) this.router.get('/search/chapters', MiscController.findChapters.bind(this)) + this.router.get('/social', MiscController.getSocialStats.bind(this)) this.router.get('/tags', MiscController.getAllTags.bind(this)) } From eecd1c714b505c3ef3ae572ca564ccb81efd2147 Mon Sep 17 00:00:00 2001 From: benonymity Date: Wed, 6 Jul 2022 17:44:41 -0400 Subject: [PATCH 02/10] fix: Get most recent item instead of oldest --- server/controllers/MiscController.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index f8ed93f03..e51e7d12b 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -270,13 +270,13 @@ class MiscController { for (let i = 0; i < filteredUsers.length; i++) { var user = filteredUsers[i] var listeningStats = await this.getUserListeningStatsHelpers(user.id) - var latestItem = user.mediaProgress.sort(p => p.lastUpdate)[0] + var latestItem = user.mediaProgress.sort(p => p.lastUpdate) var totalItems = Object.keys(listeningStats.items).length var totalDays = Object.keys(listeningStats.days).length var session = { username: user.username, lastSeen: user.lastSeen, - latest: latestItem || null, + latest: latestItem[latestItem.length - 1] || null, itemsRead: totalItems || 0, minutesListened: listeningStats.totalTime || 0, daysListened: totalDays| 0, From 1f2b1abee060452d290211482094c73a206d0f00 Mon Sep 17 00:00:00 2001 From: benonymity Date: Wed, 6 Jul 2022 20:05:34 -0400 Subject: [PATCH 03/10] feat: Add global toggle --- client/components/app/SideRail.vue | 9 +++++---- client/pages/account.vue | 9 +++++---- client/pages/config/index.vue | 13 ++++++++++++- client/pages/social.vue | 13 ++++--------- server/controllers/MiscController.js | 8 +++++++- server/objects/settings/ServerSettings.js | 3 +++ 6 files changed, 36 insertions(+), 19 deletions(-) diff --git a/client/components/app/SideRail.vue b/client/components/app/SideRail.vue index f67383bac..0fbe56787 100644 --- a/client/components/app/SideRail.vue +++ b/client/components/app/SideRail.vue @@ -63,7 +63,7 @@
- + connect_without_contact

Social

@@ -148,14 +148,15 @@ export default { filterBy() { return this.$store.getters['user/getUserSetting']('filterBy') }, + isGlobalSharing() { + return this.$store.getters['getServerSetting']('sharedListeningStats') + }, async socialTrue() { let listeningStats = await this.$axios.$get(`/api/social`).catch((err) => { console.error('Failed to load shared user listening sesions', err) return false }) - if (listeningStats != null) { - return true - } else {return false} + return listeningStats != null ? true : false }, showingIssues() { if (!this.$route.query) return false diff --git a/client/pages/account.vue b/client/pages/account.vue index cff547e50..5a52a80fa 100644 --- a/client/pages/account.vue +++ b/client/pages/account.vue @@ -15,7 +15,7 @@
-
+

@@ -25,7 +25,7 @@

-
+

Change Password

@@ -78,6 +78,9 @@ export default { }, isSharingActivity() { return this.$store.getters['user/getUserSetting']('shareListeningActivity') + }, + isGlobalSharing() { + return this.$store.getters['getServerSetting']('sharedListeningStats') } }, methods: { @@ -128,8 +131,6 @@ export default { }) }, changeSharingSetting() { - console.log('test') - console.log(String(!this.isSharingActivity)) this.$store.dispatch('user/updateUserSettings', { shareListeningActivity: !this.isSharingActivity }) } }, diff --git a/client/pages/config/index.vue b/client/pages/config/index.vue index 24d3d68d2..7e4b1b17f 100644 --- a/client/pages/config/index.vue +++ b/client/pages/config/index.vue @@ -48,6 +48,16 @@

Chromecast support

+
+ + +

+ Shared Listening Stats + info_outlined +

+
+
+

Display

@@ -268,7 +278,8 @@ export default { storeMetadataWithItem: 'By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders. Uses .abs file extension', coverAspectRatio: 'Prefer to use square covers over standard 1.6:1 book covers', enableEReader: 'E-reader is still a work in progress, but use this setting to open it up to all your users (or use the "Experimental Features" toggle just for use by you)', - scannerPreferOverdriveMediaMarker: 'MP3 files from Overdrive come with chapter timings embedded as custom metadata. Enabling this will use these tags for chapter timings automatically' + scannerPreferOverdriveMediaMarker: 'MP3 files from Overdrive come with chapter timings embedded as custom metadata. Enabling this will use these tags for chapter timings automatically', + shareStats: 'Allow users to share latest listening activity with other users on an opt-in basis' }, showConfirmPurgeCache: false } diff --git a/client/pages/social.vue b/client/pages/social.vue index 9baf91a72..85e54fae8 100644 --- a/client/pages/social.vue +++ b/client/pages/social.vue @@ -24,12 +24,10 @@

{{ user.username }} 

-
-

{{user.latest.lastUpdate != null ? "Last listened " + $dateDistanceFromNow(user.latest.lastUpdate) : "Never listened" }}

-
+

{{ "Last listened " + $dateDistanceFromNow(user.latest.lastUpdate) }}

-
-

{{ user.minutesListened > 0 ? "Has listened for " + $elapsedPrettyExtended(user.minutesListened) : "No listening time" }}

+
+

{{ "Has listened for " + $elapsedPrettyExtended(user.minutesListened) }}

@@ -64,7 +62,7 @@ export default { }, users() { if (!this.listeningStats) return [] - return this.listeningStats.filter(c => c.latest) + return this.listeningStats.sort(function(a, b) {return b.latest.lastUpdate - a.latest.lastUpdate}) }, userMediaProgress() { return this.user.mediaProgress || [] @@ -109,7 +107,6 @@ export default { }) } } - console.log('Loaded users shared listening data', listeningStats) this.listeningStats = listeningStats }, async getLibraryItem(user) { @@ -121,8 +118,6 @@ export default { }) }, changeSharingSetting() { - console.log('test') - console.log(String(!this.isSharingActivity)) this.$store.dispatch('user/updateUserSettings', { shareListeningActivity: !this.isSharingActivity }) this.init() } diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index e51e7d12b..adb211973 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -265,6 +265,10 @@ class MiscController { } // GET: api/social async getSocialStats(req, res) { + if (this.db.serverSettings.sharedListeningStats == false) { + return res.sendStatus(404) + } + Logger.error(this.db.serverSettings.sharedListeningStats) var filteredUsers = this.db.users.filter(c => c.settings.shareListeningActivity == true) var userData = [] for (let i = 0; i < filteredUsers.length; i++) { @@ -281,7 +285,9 @@ class MiscController { minutesListened: listeningStats.totalTime || 0, daysListened: totalDays| 0, } - userData.push(session) + if (session.minutesListened && session.latest) { + userData.push(session) + } } res.json(userData) } diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index 71c478282..d77f6622a 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -47,6 +47,7 @@ class ServerSettings { // Misc Flags this.chromecastEnabled = false + this.sharedListeningStats = false this.enableEReader = false this.dateFormat = 'MM/dd/yyyy' @@ -95,6 +96,7 @@ class ServerSettings { this.sortingIgnorePrefix = !!settings.sortingIgnorePrefix this.sortingPrefixes = settings.sortingPrefixes || ['the', 'a'] this.chromecastEnabled = !!settings.chromecastEnabled + this.sharedListeningStats = !!settings.sharedListeningStats this.enableEReader = !!settings.enableEReader this.dateFormat = settings.dateFormat || 'MM/dd/yyyy' this.logLevel = settings.logLevel || Logger.logLevel @@ -131,6 +133,7 @@ class ServerSettings { sortingIgnorePrefix: this.sortingIgnorePrefix, sortingPrefixes: [...this.sortingPrefixes], chromecastEnabled: this.chromecastEnabled, + sharedListeningStats: this.sharedListeningStats, enableEReader: this.enableEReader, dateFormat: this.dateFormat, logLevel: this.logLevel, From f64c74519b7dbf259251f328a4528f9228d5ce18 Mon Sep 17 00:00:00 2001 From: benonymity Date: Sat, 20 Aug 2022 12:01:00 -0400 Subject: [PATCH 04/10] fix: correct merge errors --- client/pages/config/index.vue | 4 ++-- server/controllers/MiscController.js | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/client/pages/config/index.vue b/client/pages/config/index.vue index f3dca20e3..cc50b6bb3 100644 --- a/client/pages/config/index.vue +++ b/client/pages/config/index.vue @@ -309,7 +309,7 @@ export default { coverAspectRatio: 'Prefer to use square covers over standard 1.6:1 book covers', enableEReader: 'E-reader is still a work in progress, but use this setting to open it up to all your users (or use the "Experimental Features" toggle just for use by you)', scannerPreferOverdriveMediaMarker: 'MP3 files from Overdrive come with chapter timings embedded as custom metadata. Enabling this will use these tags for chapter timings automatically', - shareStats: 'Allow users to share latest listening activity with other users on an opt-in basis' + shareStats: 'Allow users to share latest listening activity with other users on an opt-in basis', scannerUseSingleThreadedProber: 'The old scanner used a single thread. Leaving it in to use as a comparison for now.', scannerMaxThreads: 'Number of concurrent media files to scan at a time. Value of 1 will be a slower scan but less CPU usage.

Value of 0 defaults to # of CPU cores for this server times 2 (i.e. 4-core CPU will be 8)' }, @@ -461,4 +461,4 @@ export default { this.initServerSettings() } } - \ No newline at end of file + diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index 6f00d8320..854cbeedc 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -285,6 +285,7 @@ class MiscController { } } res.json(userData) + } validateCronExpression(req, res) { const expression = req.body.expression @@ -301,4 +302,4 @@ class MiscController { } } } -module.exports = new MiscController() \ No newline at end of file +module.exports = new MiscController() From adba22cbd613b92e70aec67c21ac1b797f1bdd38 Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 3 Sep 2022 14:23:20 -0400 Subject: [PATCH 05/10] Apply suggestions from code review Thanks for all the tweaks @advplyr! Co-authored-by: advplyr <67830747+advplyr@users.noreply.github.com> --- client/components/app/SideRail.vue | 2 +- client/pages/social.vue | 32 +--------------------------- server/controllers/MiscController.js | 3 +-- 3 files changed, 3 insertions(+), 34 deletions(-) diff --git a/client/components/app/SideRail.vue b/client/components/app/SideRail.vue index 4e0f6e7f8..b95cc0024 100644 --- a/client/components/app/SideRail.vue +++ b/client/components/app/SideRail.vue @@ -68,7 +68,7 @@

Social

-
+
diff --git a/client/pages/social.vue b/client/pages/social.vue index 85e54fae8..d0726f3e4 100644 --- a/client/pages/social.vue +++ b/client/pages/social.vue @@ -51,11 +51,8 @@ export default { } }, computed: { - coverAspectRatio() { - return this.$store.getters['getServerSetting']('coverAspectRatio') - }, bookCoverAspectRatio() { - return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE ? 1 : 1.6 + return this.$store.getters['libraries/getBookCoverAspectRatio'] }, bookCoverWidth() { return 70 @@ -64,25 +61,6 @@ export default { if (!this.listeningStats) return [] return this.listeningStats.sort(function(a, b) {return b.latest.lastUpdate - a.latest.lastUpdate}) }, - userMediaProgress() { - return this.user.mediaProgress || [] - }, - userItemsFinished() { - if (!this.listeningStats) return [] - return this.listeningStats.itemsRead - }, - mostRecentListeningSessions() { - if (!this.listeningStats) return [] - return this.listeningStats.recentSessions || [] - }, - totalMinutesListening() { - if (!this.listeningStats) return 0 - return Math.round(this.listeningStats.totalTime / 60) - }, - totalDaysListened() { - if (!this.listeningStats) return 0 - return Object.values(this.listeningStats.days).length - }, usersOnline() { var usermap = {} this.$store.state.users.users.forEach((u) => (usermap[u.id] = { online: true, session: u.session })) @@ -109,14 +87,6 @@ export default { } this.listeningStats = listeningStats }, - async getLibraryItem(user) { - user.libraryItem = await this.$axios - .$get(`/api/items/${id}`) - .catch((error) => { - console.error('Failed', error) - return false - }) - }, changeSharingSetting() { this.$store.dispatch('user/updateUserSettings', { shareListeningActivity: !this.isSharingActivity }) this.init() diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index 854cbeedc..1e03c3540 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -260,10 +260,9 @@ class MiscController { } // GET: api/social async getSocialStats(req, res) { - if (this.db.serverSettings.sharedListeningStats == false) { + if (!this.db.serverSettings.sharedListeningStats) { return res.sendStatus(404) } - Logger.error(this.db.serverSettings.sharedListeningStats) var filteredUsers = this.db.users.filter(c => c.settings.shareListeningActivity == true) var userData = [] for (let i = 0; i < filteredUsers.length; i++) { From 0125f9b3ad7f113b0cdd6ffe1afe27cc3d4990eb Mon Sep 17 00:00:00 2001 From: benonymity Date: Sat, 3 Sep 2022 15:23:58 -0400 Subject: [PATCH 06/10] fix: unnecessary v-models and extra requests --- client/components/app/SideRail.vue | 11 ++--------- client/pages/account.vue | 19 +++++++++++-------- client/pages/social.vue | 30 ++++++++++++++++++------------ 3 files changed, 31 insertions(+), 29 deletions(-) diff --git a/client/components/app/SideRail.vue b/client/components/app/SideRail.vue index b95cc0024..c86185e10 100644 --- a/client/components/app/SideRail.vue +++ b/client/components/app/SideRail.vue @@ -63,7 +63,7 @@
- + connect_without_contact

Social

@@ -155,13 +155,6 @@ export default { isGlobalSharing() { return this.$store.getters['getServerSetting']('sharedListeningStats') }, - async socialTrue() { - let listeningStats = await this.$axios.$get(`/api/social`).catch((err) => { - console.error('Failed to load shared user listening sesions', err) - return false - }) - return listeningStats != null ? true : false - }, showingIssues() { if (!this.$route.query) return false return this.libraryBookshelfPage && this.$route.query.filter === 'issues' @@ -192,4 +185,4 @@ export default { }, mounted() {} } - \ No newline at end of file + diff --git a/client/pages/account.vue b/client/pages/account.vue index 5a52a80fa..cd9a8b490 100644 --- a/client/pages/account.vue +++ b/client/pages/account.vue @@ -16,7 +16,7 @@
- +

Share Listening Activity @@ -54,7 +54,8 @@ export default { password: null, newPassword: null, confirmPassword: null, - changingPassword: false + changingPassword: false, + isUserSharing: null } }, computed: { @@ -76,14 +77,14 @@ export default { isGuest() { return this.usertype === 'guest' }, - isSharingActivity() { - return this.$store.getters['user/getUserSetting']('shareListeningActivity') - }, isGlobalSharing() { return this.$store.getters['getServerSetting']('sharedListeningStats') } }, methods: { + init() { + this.isUserSharing = this.$store.getters['user/getUserSetting']('shareListeningActivity') + }, logout() { var rootSocket = this.$root.socket || {} const logoutPayload = { @@ -131,9 +132,11 @@ export default { }) }, changeSharingSetting() { - this.$store.dispatch('user/updateUserSettings', { shareListeningActivity: !this.isSharingActivity }) + this.$store.dispatch('user/updateUserSettings', { shareListeningActivity: this.isUserSharing}) } }, - mounted() {} + mounted() { + this.init() + } } - \ No newline at end of file + diff --git a/client/pages/social.vue b/client/pages/social.vue index d0726f3e4..4794dcf8a 100644 --- a/client/pages/social.vue +++ b/client/pages/social.vue @@ -1,6 +1,6 @@