Skip to content

Commit

Permalink
Add forced index matching in bulk chapter add
Browse files Browse the repository at this point in the history
Addresses #19. When chapters are unnamed (using the default "Chapter 1",
"Chapter 2", etc), bulk add will find the chapter with timestamps
closest to the baseline. However, a user might want to always select
the fifth chapter, regardless of its timestamp. To address this, add yet
another toggle to the bulk add UI that allows users to switch between
strict index matching and closest timestamp.
  • Loading branch information
danrahn committed Feb 4, 2024
1 parent f37b2fd commit 074f5c6
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 17 deletions.
106 changes: 90 additions & 16 deletions Client/Script/BulkAddOverlay.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { BulkActionCommon, BulkActionRow, BulkActionTable, BulkActionType } from
import { BulkMarkerResolveType, MarkerData } from '../../Shared/PlexTypes.js';
import ButtonCreator from './ButtonCreator.js';
import { ContextualLog } from '../../Shared/ConsoleLog.js';
import { getSvgIcon } from './SVGHelper.js';
import Icons from './Icons.js';
import Overlay from './Overlay.js';
import { PlexClientState } from './PlexClientState.js';
Expand Down Expand Up @@ -67,6 +68,8 @@ class BulkAddOverlay {
#cachedChapterStart;
/** @type {ChapterData} Cached baseline end chapter data. */
#cachedChapterEnd;
/** @type {boolean} Determines whether to favor chapter indexes or timestamps for fuzzy matching. */
#chapterIndexMode;

/**
* List of descriptions for the various bulk marker resolution actions. */
Expand All @@ -78,6 +81,11 @@ class BulkAddOverlay {
'If any added marker conflicts with existing markers, delete the existing markers',
];

static #indexMatchingTooltip = buildNode('span',
{ class : 'smallerTooltip' },
"When an exact chapter name match isn't available, " +
"use the chapter's index to find matching chapters, not the closest timestamp");

/**
* Construct a new bulk add overlay.
* @param {ShowData|SeasonData} mediaItem */
Expand Down Expand Up @@ -128,7 +136,18 @@ class BulkAddOverlay {
buildNode('label', { for : 'addStartChapter' }, 'Start: '),
buildNode('select', { id : 'addStartChapter' }, 0, { change : this.#onChapterChanged.bind(this) }),
buildNode('label', { for : 'addEndChapter' }, 'End: '),
buildNode('select', { id : 'addEndChapter' }, 0, { change : this.#onChapterChanged.bind(this) })
buildNode('select', { id : 'addEndChapter' }, 0, { change : this.#onChapterChanged.bind(this) }),
buildNode('br'),
appendChildren(buildNode('span', { id : 'chapterIndexModeContainer' }),
buildNode('label', { for : 'chapterIndexMode' }, 'Force index matching: '),
buildNode('input',
{ type : 'checkbox', id : 'chapterIndexMode' },
0,
{ change : this.#onChapterIndexModeChanged.bind(this) }),
appendChildren(buildNode('span', { id : 'chapterIndexModeHelp' }),
getSvgIcon(Icons.Help, ThemeColors.Primary, { width : 15, height : 15 })
)
)
),
appendChildren(buildNode('div', { id : 'bulkAddInputMethod' }),
ButtonCreator.fullButton(
Expand Down Expand Up @@ -171,6 +190,7 @@ class BulkAddOverlay {
);

this.#inputMode = $('#switchInputMethod', container);
Tooltip.setTooltip($('#chapterIndexModeHelp', container), BulkAddOverlay.#indexMatchingTooltip);

Overlay.build({
dismissible : true,
Expand Down Expand Up @@ -284,6 +304,13 @@ class BulkAddOverlay {
this.#updateTableStats();
}

/**
* Update chapter index mode, i.e. whether chapter indexes or timestamps take precedence for fuzzy matching. */
#onChapterIndexModeChanged() {
this.#chapterIndexMode = $('#chapterIndexMode').checked;
this.#updateTableStats();
}

/**
* Attempt to retrieve chapter data for all episodes in this overlay. */
async #checkForChapters() {
Expand Down Expand Up @@ -601,6 +628,7 @@ class BulkAddOverlay {
chapterMode() { return $('#timeZone').classList.contains('hidden'); }
chapterStart() { return this.#cachedChapterStart; }
chapterEnd() { return this.#cachedChapterEnd; }
chapterIndexMode() { return this.#chapterIndexMode; }

/** Update all items in the customization table, if present. */
#updateTableStats() {
Expand All @@ -619,10 +647,12 @@ const ChapterMatchMode = {
NameMatch : 1,
/** @readonly We found a chapter with the same timestamp as the baseline. */
TimestampMatch : 2,
/** @readonly The user is using index mode, and we found a chapter at the same index. */
IndexMatch : 3,
/** @readonly We're returning the marker closest to the baseline, but it doesn't match exactly. */
Fuzzy : 3,
Fuzzy : 4,
/** @readonly This item has no markers. */
NoMatch : 4,
NoMatch : 5,
};

/**
Expand Down Expand Up @@ -681,26 +711,28 @@ class BulkAddRow extends BulkActionRow {

/**
* Same as #calculateStart, but for the end timestamp. */
#calculateEnd() {
return this.#calculateStartEnd('end', this.#parent.chapterEnd());
#calculateEnd(min=-1) {
return this.#calculateStartEnd('end', this.#parent.chapterEnd(), min);
}

/**
* Return the start and end timestamp for this row. */
getChapterTimestampData() {
Log.assert(this.#parent.chapterMode(), `We should be in chapterMode if we're calling getChapterTimestampData`);
const start = this.#calculateStartEnd('start', this.#parent.chapterStart()).time;
return {
start : this.#calculateStartEnd('start', this.#parent.chapterStart()).time,
end : this.#calculateStartEnd('end', this.#parent.chapterEnd()).time
start : start,
end : this.#calculateStartEnd('end', this.#parent.chapterEnd(), start).time
};
}

/**
* Find the best timestamp for this row based on the bulk add type (raw/chapter)
* @param {'start'|'end'} type
* @param {ChapterData} Baseline
* @param {ChapterData} baseline
* @param {number} min
* @returns {{ mode : number, time : number }} */
#calculateStartEnd(type, baseline) {
#calculateStartEnd(type, baseline, min=-1) {
if (!this.#parent.chapterMode()) {
return {
mode : ChapterMatchMode.Disabled,
Expand Down Expand Up @@ -734,10 +766,33 @@ class BulkAddRow extends BulkActionRow {
};
}

// If no name match, find the chapter with the closest timestamp.
let fuzzyTime = this.#chapters[0][type];
for (const chapter of this.#chapters) {
const chapterTime = chapter[type];
// If the user selects 'index mode', ignore timestamps and just
// pick the chapter with the same index (if it exists)
if (this.#parent.chapterIndexMode() && this.#chapters.length > baseline.index) {
return {
mode : ChapterMatchMode.IndexMatch,
time : this.#chapters[baseline.index][type]
};
}

// If no name match, find the chapter with the closest timestamp, or index.
let index = 0;
while (index < this.#chapters.length && this.#chapters[index][type] <= min) {
++index;
}

// All timestamps are less than the minimum. This shouldn't happen, but just
// return the last chapter.
if (index === this.#chapters.length) {
return {
mode : ChapterMatchMode.Fuzzy,
time : this.#chapters[this.#chapters.length - 1][type],
};
}

let fuzzyTime = this.#chapters[index][type];
for (; index < this.#chapters.length; ++index) {
const chapterTime = this.#chapters[index][type];
if (chapterTime === baselineTime) {
return {
mode : ChapterMatchMode.TimestampMatch,
Expand Down Expand Up @@ -769,8 +824,8 @@ class BulkAddRow extends BulkActionRow {
this.row.classList.remove('bulkActionInactive');

const startData = this.#calculateStart();
const endData = this.#calculateEnd();
const startTimeBase = startData.time;
const endData = this.#calculateEnd(startTimeBase);
const endTimeBase = endData.time;
const resolveType = this.#parent.resolveType();
const warnClass = resolveType === BulkMarkerResolveType.Merge ? 'bulkActionSemi' : 'bulkActionOff';
Expand Down Expand Up @@ -886,6 +941,7 @@ class BulkAddRow extends BulkActionRow {
* @param {number} startMode
* @param {number} endMode
* @returns {void} */
// eslint-disable-next-line complexity
#setChapterMatchModeText(startMode, endMode) {
// In "normal" mode - nothing extra to do.
if (startMode === ChapterMatchMode.Disabled) {
Expand Down Expand Up @@ -915,6 +971,7 @@ class BulkAddRow extends BulkActionRow {
case ChapterMatchMode.NameMatch:
return setTitleInfo('bulkActionOn', 'This episode has matching chapter name data.');
case ChapterMatchMode.TimestampMatch:
case ChapterMatchMode.IndexMatch:
return setTitleInfo('bulkActionOn', 'This episode has matching chapter data.');
case ChapterMatchMode.Fuzzy:
return setTitleInfo('bulkActionSemi',
Expand All @@ -925,6 +982,7 @@ class BulkAddRow extends BulkActionRow {
case ChapterMatchMode.TimestampMatch:
switch (endMode) {
case ChapterMatchMode.NameMatch:
case ChapterMatchMode.IndexMatch:
return setTitleInfo('bulkActionOn', 'This episode has matching chapter data.');
case ChapterMatchMode.TimestampMatch:
return setTitleInfo('bulkActionOn', 'This episode has matching chapter timestamp data.');
Expand All @@ -934,14 +992,30 @@ class BulkAddRow extends BulkActionRow {
default:
return Log.warn(`Unexpected ChapterMatchMode ${endMode}`);
}
case ChapterMatchMode.IndexMatch:
switch (endMode) {
case ChapterMatchMode.TimestampMatch:
case ChapterMatchMode.NameMatch:
return setTitleInfo('bulkActionOn', 'This episode has matching chapter data.');
case ChapterMatchMode.IndexMatch:
return setTitleInfo('bulkActionOn', 'This episode has matching chapter index data.');
case ChapterMatchMode.Fuzzy:
return setTitleInfo('bulkActionSemi',
'Start chapter has an index match, but end chapter does not, using closest timestamp.');
default:
return Log.warn(`Unexpected ChapterMatchMode ${endMode}`);
}
case ChapterMatchMode.Fuzzy:
switch (endMode) {
case ChapterMatchMode.NameMatch:
return setTitleInfo('bulkActionSemi',
'End chapter has a match, but start does not, using closest timestamp.');
'End chapter has a match, but start does not. Using closest timestamp.');
case ChapterMatchMode.TimestampMatch:
return setTitleInfo('bulkActionSemi',
'End chapter has a timestamp match, but start does not, using closest timestamp.');
'End chapter has a timestamp match, but start does not. Using closest timestamp.');
case ChapterMatchMode.IndexMatch:
return setTitleInfo('bulkActionSemi',
'End chapter has an index match, but start does not. Using closest timestamp');
case ChapterMatchMode.Fuzzy:
return setTitleInfo('bulkActionSemi', 'No matching chapters found, using chapter with the closest timestamps.');
default:
Expand Down
24 changes: 24 additions & 0 deletions Client/Style/BulkActionOverlay.css
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,28 @@
/** Icon-based buttons don't have a border by default, but we want one in the overlay. */
border: 1px solid var(--bulk-action-button-border);
}

& #chapterIndexModeContainer {
font-size: smaller;

& input[type=checkbox] {
margin: 0;
vertical-align: text-bottom;
}

& #chapterIndexModeHelp {
margin: 0 0 0 5px;
padding: 0;
vertical-align: text-bottom;
opacity: 0.8;
}

& #chapterIndexModeHelp:hover {
opacity: 1;
}

& #chapterIndexModeHelp svg {
vertical-align: text-bottom;
}
}
}
1 change: 1 addition & 0 deletions Server/PlexQueryManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -1896,6 +1896,7 @@ INNER JOIN metadata_items b ON media_items.metadata_item_id=b.id`;

chapters.push({
name : chapter.name,
index : chapters.length,
start : start,
end : parseInt(chapter.end * 1000)
});
Expand Down
2 changes: 1 addition & 1 deletion Shared/PlexTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ import { MarkerType } from './MarkerType.js';
* @typedef {{[metadataId: number]: MarkerData[] }} MarkerDataMap
* @typedef {{[metadataId: number]: SerializedMarkerData[] }} SerializedMarkerDataMap
*
* @typedef {{ name : string, start : number, end : number }} ChapterData
* @typedef {{ name : string, index : number, start : number, end : number }} ChapterData
* @typedef {{ [metadataId: number]: ChapterData[] }} ChapterMap
* @typedef {{ [episodeId: number]: { start : number, end : number } }} CustomBulkAddMap
*
Expand Down

0 comments on commit 074f5c6

Please sign in to comment.