Skip to content

Implement playback inactivity timeout #6686

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 25 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
7c2b62e
spitballing on how to stub out the plugin
simonerlic Mar 18, 2025
2092bf4
add notes to the imports
simonerlic Mar 18, 2025
11f1030
Merge pull request #1 from simonerlic/stillWatchingPlugin-stub
simonerlic Mar 18, 2025
7b887ec
Change plugin to js from ts due to TS5097
simonerlic Mar 20, 2025
da0c03d
Delete plugin.ts
simonerlic Mar 20, 2025
73aaf77
first attempt
mefrese Mar 25, 2025
d491610
implented timeout plugin logic
reiddouglas Mar 26, 2025
c85996b
added setting check to time-based timeout in onPlayerTimeUpdate
reiddouglas Mar 26, 2025
51b2214
second attempt
mefrese Mar 27, 2025
f37e54b
third attempt
mefrese Mar 27, 2025
e64c0e6
attempt whatever
mefrese Mar 27, 2025
49f5c74
Add internationalisation strings for new settings
simonerlic Mar 29, 2025
21399d5
Fix show/hide settings, add minutes setting, tidy up code
simonerlic Mar 29, 2025
bc1efe3
Update userSettings timeout comment for parameter
simonerlic Mar 29, 2025
ca508a3
removed old setting names causing a null reference error
reiddouglas Mar 30, 2025
003e57a
Refactor still watching mode: save both values, display only active mode
Mar 30, 2025
c702633
changed file to typescript and removed setting stubs
reiddouglas Mar 30, 2025
eda62d5
Merge pull request #2 from MatthewM46/megsbranch
reiddouglas Mar 31, 2025
433717f
Merge branch 'megsbranch' into still-watching-plugin-impl
reiddouglas Mar 31, 2025
1e8edd5
changed getter functions and episodal timeout logic
reiddouglas Mar 31, 2025
5c83093
added translation support for timeout prompt
reiddouglas Mar 31, 2025
52e4883
added jsdoc
reiddouglas Mar 31, 2025
f25665d
Clarified unit of time used for timeout within select container
reiddouglas Mar 31, 2025
efd70d0
Add description to the timeout modal
simonerlic Mar 31, 2025
a8dde66
Merge pull request #3 from simonerlic/still-watching-plugin-impl
simonerlic Mar 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/components/playback/playbackmanager.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { getMediaError } from 'utils/mediaError';
import { toApi } from 'utils/jellyfin-apiclient/compat';
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind.js';
import { bindSkipSegment } from './skipsegment.ts';
import { bindPlayTimeout } from '../../plugins/playTimeout/playTimeout.ts';

const UNLIMITED_ITEMS = -1;

Expand Down Expand Up @@ -3698,6 +3699,7 @@ export class PlaybackManager {

bindMediaSegmentManager(self);
this._skipSegment = bindSkipSegment(self);
this._playTimeout = bindPlayTimeout(self);
}

getCurrentPlayer() {
Expand Down
76 changes: 75 additions & 1 deletion src/components/playbackSettings/playbackSettings.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,29 @@ function setMaxBitrateFromField(select, isInNetwork, mediatype) {
}
}

function showHideStillWatchingOptions(context) {
const stillWatchingEnabled = context.querySelector('.chkStillWatching').checked;
const optionsContainer = context.querySelector('.stillWatchingOptions');

if (stillWatchingEnabled) {
optionsContainer.classList.remove('hide');
} else {
optionsContainer.classList.add('hide');
}
Comment on lines +147 to +151
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (stillWatchingEnabled) {
optionsContainer.classList.remove('hide');
} else {
optionsContainer.classList.add('hide');
}
optionsContainer.classList.toggle('hide', !stillWatchingEnabled);

}

function initStillWatchingControls(context) {
const modeSelect = context.querySelector('#selectStillWatchingMode');
const episodeContainer = context.querySelector('.episodeContainer');
const timeContainer = context.querySelector('.timeContainer');

modeSelect.addEventListener('change', () => {
const isTimeMode = modeSelect.value === 'time';
episodeContainer.classList.toggle('hide', isTimeMode);
timeContainer.classList.toggle('hide', !isTimeMode);
});
}

function showHideQualityFields(context, user, apiClient) {
if (user.Policy.EnableVideoPlaybackTranscoding) {
context.querySelector('.videoQualitySection').classList.remove('hide');
Expand Down Expand Up @@ -185,6 +208,7 @@ function loadForm(context, user, userSettings, systemInfo, apiClient) {
const userId = user.Id;

showHideQualityFields(context, user, apiClient);
showHideStillWatchingOptions(context);

if (browser.safari) {
context.querySelector('.fldEnableHi10p').classList.remove('hide');
Expand Down Expand Up @@ -225,6 +249,12 @@ function loadForm(context, user, userSettings, systemInfo, apiClient) {
context.querySelector('.chkEnableHi10p').checked = appSettings.enableHi10p();
context.querySelector('.chkEnableCinemaMode').checked = userSettings.enableCinemaMode();
context.querySelector('#selectAudioNormalization').value = userSettings.selectAudioNormalization();

context.querySelector('.chkStillWatching').checked = userSettings.enableStillWatching();
context.querySelector('#stillWatchingEpisodeCount').value = userSettings.askAfterNumEpisodes() || 1;
displayStillWatchingMode(context, userSettings);
showHideStillWatchingOptions(context);

context.querySelector('.chkEnableNextVideoOverlay').checked = userSettings.enableNextVideoInfoOverlay();
context.querySelector('.chkRememberAudioSelections').checked = user.Configuration.RememberAudioSelections || false;
context.querySelector('.chkRememberSubtitleSelections').checked = user.Configuration.RememberSubtitleSelections || false;
Expand Down Expand Up @@ -265,6 +295,7 @@ function loadForm(context, user, userSettings, systemInfo, apiClient) {
populateMediaSegments(mediaSegmentContainer, userSettings);

loading.hide();
initStillWatchingControls(context);
Comment on lines 297 to +298
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's hide the loading indicator at the end.

Suggested change
loading.hide();
initStillWatchingControls(context);
initStillWatchingControls(context);
loading.hide();

}

function saveUser(context, user, userSettingsInstance, apiClient) {
Expand Down Expand Up @@ -292,6 +323,16 @@ function saveUser(context, user, userSettingsInstance, apiClient) {
user.Configuration.AudioLanguagePreference = context.querySelector('#selectAudioLanguage').value;
user.Configuration.PlayDefaultAudioTrack = context.querySelector('.chkPlayDefaultAudioTrack').checked;
user.Configuration.EnableNextEpisodeAutoPlay = context.querySelector('.chkEpisodeAutoPlay').checked;

userSettingsInstance.enableStillWatching(context.querySelector('.chkStillWatching').checked);
const mode = context.querySelector('#selectStillWatchingMode').value;

userSettingsInstance.timeBasedStillWatching(mode === 'time');
if (mode === 'time') {
userSettingsInstance.stillWatchingTimeout(context.querySelector('#stillWatchingTime').value);
} else {
userSettingsInstance.askAfterNumEpisodes(context.querySelector('#stillWatchingEpisodeCount').value);
}
userSettingsInstance.preferFmp4HlsContainer(context.querySelector('.chkPreferFmp4HlsContainer').checked);
userSettingsInstance.enableCinemaMode(context.querySelector('.chkEnableCinemaMode').checked);
userSettingsInstance.selectAudioNormalization(context.querySelector('#selectAudioNormalization').value);
Expand Down Expand Up @@ -347,9 +388,13 @@ function onSubmit(e) {

function embed(options, self) {
options.element.innerHTML = globalize.translateHtml(template, 'core');

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please revert unnecessary changes (blank line removal).

options.element.querySelector('form').addEventListener('submit', onSubmit.bind(self));

const stillWatchingCheckbox = options.element.querySelector('.chkStillWatching');
stillWatchingCheckbox.addEventListener('change', () => {
showHideStillWatchingOptions(options.element);
});

if (options.enableSaveButton) {
options.element.querySelector('.btnSave').classList.remove('hide');
}
Expand All @@ -361,6 +406,35 @@ function embed(options, self) {
}
}

function displayStillWatchingMode(context, userSettings) {
// Determine saved mode
const isTimeMode = userSettings.timeBasedStillWatching?.() === true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need optional chain for userSettings.timeBasedStillWatching?

const mode = isTimeMode ? 'time' : 'episodes';

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better to make time and episodes consts.

// Set mode select dropdown
const modeSelect = context.querySelector('#selectStillWatchingMode');
modeSelect.value = mode;

// Containers
const episodeContainer = context.querySelector('.episodeContainer');
const timeContainer = context.querySelector('.timeContainer');

// Toggle visibility
episodeContainer.classList.toggle('hide', isTimeMode);
timeContainer.classList.toggle('hide', !isTimeMode);

// Inputs
const timeInput = context.querySelector('#stillWatchingTime');
const episodeInput = context.querySelector('#stillWatchingEpisodeCount');

// Populate only the relevant field
if (isTimeMode) {
timeInput.value = userSettings.stillWatchingTimeout() || 300;
} else {
episodeInput.value = userSettings.askAfterNumEpisodes() || 5;
}
Copy link
Contributor

@dmitrylyzo dmitrylyzo Mar 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You already have default values.

return parseInt(this.get('stillWatchingTimeout') || '60', 10);

return parseInt(this.get('askAfterNumEpisodes') || '0', 10);

Normally we put the default value in the setting getter.

}

class PlaybackSettings {
constructor(options) {
this.options = options;
Expand Down
Loading
Loading