feat!: improve chapter ranges serialization

Users can no longer create their own or edit existing chapter range serializers, but that was always very limited and prone to errors. Instead, all common use cases for chapter ranges: intros/openings, outros/endings, and sponsor blocks serializers are now internal.

Users can still disable and configure colors for each individual serializer by specifying a `{type}:{color}` pairs delimited by a comma on a `chapter_ranges` property. The color is now an RGB(A) HEX code.
This commit is contained in:
tomasklaen
2022-09-21 22:58:00 +02:00
parent a03fe26f38
commit a4638fcdef
2 changed files with 86 additions and 158 deletions

View File

@@ -171,48 +171,17 @@ font_height_to_letter_width_ratio=0.5
# Default open-file menu directory
default_directory=~/
# `chapter_ranges` lets you transform chapter indicators into range indicators.
# Convers some common chapter types into chapter range indicators.
# Instead of displaying the start of the chapter as a diamond icon on top of the
# timeline, the portion of the timeline owned by that chapter is colored based
# on the config below.
#
# Chapter range definition syntax:
# ```
# start_pattern<color:opacity>end_pattern
# ```
# The syntax is a comma delimited list of `{type}:{color}` pairs, where.
# `{type}` - range type. Currently suported ones are:
# - `openings` - intors and anime openings
# - `endings` - outros and anime endings
# - `ads` - sponsor blocks created by script: https://github.com/po5/mpv_sponsorblock
# `{color}` - an RGB(A) HEX color code (`rrggbb`, or `rrggbbaa`)
#
# Multiple start and end patterns can be defined by separating them with `|`:
# ```
# p1|pN<color:opacity>p1|pN
# ```
#
# Multiple chapter ranges can be defined by separating them with comma:
#
# chapter_ranges=range1,rangeN
#
# One of `start_pattern`s can be a custom keyword `{bof}` that will match
# beginning of file when it makes sense.
#
# One of `end_pattern`s can be a custom keyword `{eof}` that will match end of
# file when it makes sense.
#
# Patterns are lua patterns (http://lua-users.org/wiki/PatternsTutorial).
# They only need to occur in a title, not match it completely.
# Matching is case insensitive.
#
# Chapters that end a range and got matched by an `end_pattern` other then `.*`
# will not be shown on hover, unless that chapter is also used to start another
# range.
#
# `color` is a `bbggrr` hexadecimal color code.
# `opacity` is a float number from 0 to 1.
#
# Examples:
#
# Display anime openings and endings as ranges:
# ```
# chapter_ranges=^op| op$|opening<968638:0.5>.*, ^ed| ed$|^end|ending$<968638:0.5>.*|{eof}
# ```
#
# Display skippable youtube video sponsor blocks from https://github.com/po5/mpv_sponsorblock
# ```
# chapter_ranges=sponsor start<3535a5:.5>sponsor end, segment start<3535a5:0.5>segment end
# ```
chapter_ranges=^op |^op$| op$|opening$<968638:0.5>.*, ^ed |^ed$| ed$|ending$<968638:0.5>.*|{eof}, sponsor start<3535a5:.5>sponsor end, segment start<3535a5:0.5>segment end, ^sponsors?<3535a5:.5>.*, ^intro$<968638:0.5>.*, ^outro$<968638:0.5>.*|{eof}
# To not convert any of the range types, simply removed it from the list.
chapter_ranges=openings:38869680,endings:38869680,ads:a5353580

View File

@@ -231,7 +231,7 @@ local options = {
subtitle_types = 'aqt,ass,gsub,idx,jss,lrc,mks,pgs,pjs,psb,rt,slt,smi,sub,sup,srt,ssa,ssf,ttxt,txt,usf,vt,vtt',
font_height_to_letter_width_ratio = 0.5,
default_directory = '~/',
chapter_ranges = '^op| op$|opening<968638:0.5>.*, ^ed| ed$|^end|ending$<968638:0.5>.*|{eof}, sponsor start<3535a5:.5>sponsor end, segment start<3535a5:0.5>segment end',
chapter_ranges = 'openings:38869680,endings:38869680,ads:a5353580',
}
opt.read_options(options, 'uosc')
-- Normalize values
@@ -342,42 +342,16 @@ local config = {
}
end
end)(),
chapter_serializers = (function()
-- Parse `chapter_ranges` option into workable data structure
local chapter_ranges = {}
for _, definition in ipairs(split(options.chapter_ranges, ' *,+ *')) do
local start_patterns, color, opacity, end_patterns = string.match(
definition, '([^<]+)<(%x%x%x%x%x%x):(%d?%.?%d*)>([^>]+)'
)
-- Valid definition
if start_patterns then
start_patterns = start_patterns:lower()
end_patterns = end_patterns:lower()
local uses_bof = start_patterns:find('{bof}') ~= nil
local uses_eof = end_patterns:find('{eof}') ~= nil
local chapter_range = {
start_patterns = split(start_patterns, '|'),
end_patterns = split(end_patterns, '|'),
color = color,
opacity = tonumber(opacity),
ranges = {},
uses_bof = uses_bof,
uses_eof = uses_eof,
}
-- Filter out special keywords so we don't use them when matching titles
if uses_bof then
chapter_range.start_patterns = itable_remove(chapter_range.start_patterns, '{bof}')
end
if uses_eof and chapter_range.end_patterns then
chapter_range.end_patterns = itable_remove(chapter_range.end_patterns, '{eof}')
end
chapter_ranges[#chapter_ranges + 1] = chapter_range
chapter_ranges = (function()
---@type table<string, {color: string; opacity: number}>
local ranges = {}
if options.chapter_ranges and options.chapter_ranges ~= '' then
for _, definition in ipairs(split(options.chapter_ranges or '', ' *,+ *')) do
local type_color = split(definition, ' *:+ *')
ranges[type_color[1]] = serialize_rgba(type_color[2])
end
end
return chapter_ranges
return ranges
end)(),
}
-- Adds `{element}_persistency` property with table of flags when the element should be visible (`{paused = true}`)
@@ -817,79 +791,72 @@ function delete_file(path)
})
end
function serialize_chapter_ranges(chapters)
state.chapter_ranges = {}
function serialize_chapter_ranges(normalized_chapters)
local ranges = {}
local chapters = {}
local simple_ranges = {
{name = 'openings', patterns = {'^op', '^op$', 'op$', 'opening$', '^intro$'}, requires_next_chapter = true},
{name = 'endings', patterns = {'^ed', '^ed$', 'ed$', 'ending$', '^outro$'}},
}
for _, chapter_serializer in ipairs(config.chapter_serializers) do
local current_range = nil
-- bof and eof should be used only once per timeline
-- eof is only used when last range is missing end
local bof_used = false
for i, normalized_chapter in ipairs(normalized_chapters) do
local chapter = table_shallow_copy(normalized_chapter)
chapters[i] = chapter
local function start_range(chapter)
-- If there is already a range started, should we append or overwrite?
-- I chose overwrite here.
current_range = {['start'] = chapter}
end
local function end_range(chapter)
current_range['end'] = chapter
current_range.color = chapter_serializer.color
current_range.opacity = chapter_serializer.opacity
state.chapter_ranges[#state.chapter_ranges + 1] = current_range
-- Mark both chapter objects
current_range['start']._uosc_used_as_range_point = true
current_range['end']._uosc_used_as_range_point = true
-- Clear for next range
current_range = nil
end
for _, chapter in ipairs(chapters) do
if type(chapter.title) == 'string' then
local lowercase_title = chapter.title:lower()
-- Is ending check and handling
if chapter_serializer.end_patterns then
for _, end_pattern in ipairs(chapter_serializer.end_patterns) do
if lowercase_title:find(end_pattern) then
if current_range == nil and chapter_serializer.uses_bof and not bof_used then
bof_used = true
start_range({time = 0})
end
if current_range ~= nil then
end_range(chapter)
chapter.is_end_only = end_pattern ~= '.*'
end
break
end
-- Simple ranges
for _, meta in ipairs(simple_ranges) do
if config.chapter_ranges[meta.name] then
local match = itable_find(meta.patterns, function(p) return chapter.lowercase_title:find(p) end)
if match then
local next_chapter = normalized_chapters[i + 1]
if next_chapter or not meta.requires_next_chapter then
ranges[#ranges + 1] = table_assign({
start = chapter.time,
['end'] = next_chapter and next_chapter.time or infinity,
}, config.chapter_ranges[meta.name])
chapter.is_range_point = true
if next_chapter then next_chapter.is_range_point = true end
end
end
end
end
-- Is start check and handling
for _, start_pattern in ipairs(chapter_serializer.start_patterns) do
if lowercase_title:find(start_pattern) and current_range == nil then
start_range(chapter)
chapter.is_end_only = false
-- Sponsor blocks
if config.chapter_ranges.ads then
local id = chapter.lowercase_title:match('segment start *%(([%w]%w-)%)')
if id then
for j = i + 1, #normalized_chapters, 1 do
local end_chapter = normalized_chapters[j]
local end_match = end_chapter.lowercase_title:match('segment end *%(' .. id .. '%)')
if end_match then
ranges[#ranges + 1] = table_assign({
start = chapter.time,
['end'] = end_chapter.time,
}, config.chapter_ranges.ads)
chapter.is_range_point = true
end_chapter.is_range_point = true
end_chapter.is_end_only = true
break
end
end
end
end
-- If there is an unfinished range and range type accepts eof, use it
if current_range ~= nil and chapter_serializer.uses_eof then
end_range({time = infinity})
end
end
return chapters, ranges
end
-- Ensures chapters are in chronological order
function normalize_chapters(chapters)
if not chapters then return end
-- Ensure chronological order of chapters
if not chapters then return {} end
-- Ensure chronological order
table.sort(chapters, function(a, b) return a.time < b.time end)
-- Ensure titles
for index, chapter in ipairs(chapters) do
chapter.title = chapter.title or ('Chapter ' .. index)
chapter.lowercase_title = chapter.title:lower()
end
return chapters
end
@@ -2797,12 +2764,10 @@ function Timeline:render()
-- Custom ranges
for _, chapter_range in ipairs(state.chapter_ranges) do
local rax = time_ax + time_width * (chapter_range['start'].time / state.duration)
local rbx = time_ax + time_width * math.min(chapter_range['end'].time / state.duration, 1)
-- for 1px chapter size, use the whole size of the bar including padding
local ray = size <= 1 and bay or fay
local rby = size <= 1 and bby or fby
ass:rect(rax, ray, rbx, rby, {color = chapter_range.color, opacity = chapter_range.opacity})
local rax = chapter_range.start < 0.1 and 0 or time_ax + time_width * (chapter_range.start / state.duration)
local rbx = chapter_range['end'] > state.duration - 0.1 and bbx
or time_ax + time_width * math.min(chapter_range['end'] / state.duration, 1)
ass:rect(rax, fay, rbx, fby, {color = chapter_range.color, opacity = chapter_range.opacity})
end
-- Chapters
@@ -2831,7 +2796,7 @@ function Timeline:render()
if state.chapters ~= nil then
for i, chapter in ipairs(state.chapters) do
if not chapter._uosc_used_as_range_point then draw_chapter(chapter.time) end
if not chapter.is_range_point then draw_chapter(chapter.time) end
end
end
@@ -2879,16 +2844,6 @@ function Timeline:render()
-- Hovered time and chapter
if (self.proximity_raw == 0 or self.pressed) and not (Elements.speed and Elements.speed.dragging) then
local hovered_seconds = self:get_time_at_x(cursor.x)
local chapter_title, chapter_title_width
if state.chapters then
local _, chapter = itable_find(state.chapters, function(c) return hovered_seconds >= c.time end, true)
if chapter and not chapter.is_end_only then
chapter_title = chapter.title_wrapped
chapter_title_width = chapter.title_wrapped_width
chapter_title_lines = chapter.title_wrapped_lines
end
end
-- Cursor line
-- 0.5 to switch when the pixel is half filled in
@@ -2922,11 +2877,14 @@ function Timeline:render()
end
-- Chapter title
if chapter_title then
ass:tooltip(tooltip_anchor, chapter_title, {
size = self.font_size, offset = 10, responsive = false, bold = true,
text_length_override = chapter_title_width,
})
if #state.chapters > 0 then
local _, chapter = itable_find(state.chapters, function(c) return hovered_seconds >= c.time end, true)
if chapter and not chapter.is_end_only then
ass:tooltip(tooltip_anchor, chapter.title_wrapped, {
size = self.font_size, offset = 10, responsive = false, bold = true,
text_length_override = chapter.title_wrapped_width,
})
end
end
end
@@ -4179,9 +4137,10 @@ mp.observe_property('track-list', 'native', function(name, value)
Elements:trigger('dispositions')
end)
mp.observe_property('chapter-list', 'native', function(_, chapters)
local chapters = serialize_chapters(chapters)
local chapters, chapter_ranges = serialize_chapters(chapters), {}
if chapters then chapters, chapter_ranges = serialize_chapter_ranges(chapters) end
set_state('chapters', chapters)
serialize_chapter_ranges(chapters)
set_state('chapter_ranges', chapter_ranges)
set_state('has_chapter', #chapters > 0)
Elements:trigger('dispositions')
end)