[vlc-devel] [RFC Patch v2 2/2] stream filter: Add rust based cuesheet module
Thomas Guillem
thomas at gllm.fr
Mon Sep 14 12:56:50 CEST 2020
On Fri, Sep 11, 2020, at 21:33, Kartik Ohri wrote:
> The module is written using Rust API and serves as
> a proof of concept for the same.
> ---
> modules/demux/Makefile.am | 26 +++
> modules/demux/playlist/cuesheet.c | 192 ++++++++++++++++++
> modules/demux/playlist/cuesheet/Cargo.toml | 13 ++
> modules/demux/playlist/cuesheet/cbindgen.toml | 16 ++
> modules/demux/playlist/cuesheet/src/capi.rs | 122 +++++++++++
> modules/demux/playlist/cuesheet/src/config.rs | 106 ++++++++++
> modules/demux/playlist/cuesheet/src/lib.rs | 5 +
> modules/demux/playlist/cuesheet/src/parse.rs | 75 +++++++
> 8 files changed, 555 insertions(+)
> create mode 100644 modules/demux/playlist/cuesheet.c
> create mode 100644 modules/demux/playlist/cuesheet/Cargo.toml
> create mode 100644 modules/demux/playlist/cuesheet/cbindgen.toml
> create mode 100644 modules/demux/playlist/cuesheet/src/capi.rs
> create mode 100644 modules/demux/playlist/cuesheet/src/config.rs
> create mode 100644 modules/demux/playlist/cuesheet/src/lib.rs
> create mode 100644 modules/demux/playlist/cuesheet/src/parse.rs
>
> diff --git a/modules/demux/Makefile.am b/modules/demux/Makefile.am
> index 675b2d10af..31591a768b 100644
> --- a/modules/demux/Makefile.am
> +++ b/modules/demux/Makefile.am
> @@ -1,3 +1,4 @@
> +
> demuxdir = $(pluginsdir)/demux
> demux_LTLIBRARIES =
>
> @@ -256,6 +257,31 @@ libplaylist_plugin_la_SOURCES = \
> demux/playlist/playlist.c demux/playlist/playlist.h
> demux_LTLIBRARIES += libplaylist_plugin.la
>
> +if BUILD_RUST
> +CARGO_C = cargo capi install --release
> +RUST_RUNTIME_LIBS = -ldl -lrt -lpthread -lgcc_s -lc -lm -lrt -lpthread
> -lutil
> +libcuesheet_rust_plugin_la_SOURCES = \
> + demux/playlist/cuesheet/src/capi.rs \
> + demux/playlist/cuesheet/src/config.rs \
> + demux/playlist/cuesheet/src/lib.rs \
> + demux/playlist/cuesheet/src/parse.rs
> +
> +abs_src_dir = $(shell readlink -f $(srcdir))
> +
> + at abs_top_builddir@/modules/demux/playlist/cuesheet/cuesheet.h:
> $(libcuesheet_rust_plugin_la_SOURCES)
> + mkdir -p demux/playlist/cuesheet && cd demux/playlist/cuesheet && \
> + $(CARGO_C) --library-type=staticlib --destdir=. --includedir=..
> --libdir=. \
> + --manifest-path=$(abs_src_dir)/demux/playlist/cuesheet/Cargo.toml
> +
> +BUILT_SOURCES +=
> @abs_top_builddir@/modules/demux/playlist/cuesheet/cuesheet.h
> +CUESHEET_LIBS =
> @abs_top_builddir@/modules/demux/playlist/cuesheet/libcuesheet.a
> +CUESHEET_LIBS += $(RUST_RUNTIME_LIBS)
> +libcuesheet_plugin_la_SOURCES = demux/playlist/cuesheet.c
> @abs_top_builddir@/modules/demux/playlist/cuesheet/cuesheet.h
> +libcuesheet_plugin_la_LIBADD = $(CUESHEET_LIBS)
> +
> +demux_LTLIBRARIES += libcuesheet_plugin.la
> +endif
> +
> libts_plugin_la_SOURCES = demux/mpeg/ts.c demux/mpeg/ts.h \
> demux/mpeg/ts_pid.h demux/mpeg/ts_pid_fwd.h
> demux/mpeg/ts_pid.c \
> demux/mpeg/ts_psi.h demux/mpeg/ts_psi.c \
> diff --git a/modules/demux/playlist/cuesheet.c
> b/modules/demux/playlist/cuesheet.c
> new file mode 100644
> index 0000000000..9634396b88
> --- /dev/null
> +++ b/modules/demux/playlist/cuesheet.c
> @@ -0,0 +1,192 @@
> +/*****************************************************************************
> + * cuesheet.c : cue sheet playlist import format
> +
> *****************************************************************************
> + * Copyright (C) 2020 VLC authors and VideoLAN
> + *
> + * Authors: Kartik Ohri <kartikohri13 at gmail.com>
> + *
> + * This program is free software; you can redistribute it and/or
> modify it
> + * under the terms of the GNU Lesser General Public License as
> published by
> + * the Free Software Foundation; either version 2.1 of the License, or
> + * (at your option) any later version.
> + *
> + * This program is distributed in the hope that it will be useful,
> + * but WITHOUT ANY WARRANTY; without even the implied warranty of
> + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
> + * GNU Lesser General Public License for more details.
> + *
> + * You should have received a copy of the GNU Lesser General Public
> License
> + * along with this program; if not, write to the Free Software
> Foundation,
> + * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
> +
> *****************************************************************************/
> +
> +#ifdef HAVE_CONFIG_H
> +# include "config.h"
> +#endif
> +
> +#include <string.h>
> +
> +#include <vlc_common.h>
> +#include <vlc_plugin.h>
> +#include <vlc_access.h>
> +#include <vlc_url.h>
> +
> +#include "cuesheet/cuesheet.h"
> +#include "playlist.h"
> +
> +typedef struct {
> + struct CCuesheet *cuesheet;
> +} cuesheet_sys_t;
> +
> +int Import_Cue_Sheet(vlc_object_t *);
> +static int ReadDir(stream_t *, input_item_node_t *);
> +static char* rust_string_to_c_string(char*);
> +static char* get_cuesheet_property(CCuesheet *, const char*);
> +static char* get_cuesheet_comment(CCuesheet *, const char*);
> +static char* get_track_property(CCuesheet *, const char*, int);
> +
> +int Import_Cue_Sheet( vlc_object_t *p_this )
> +{
> + stream_t *p_demux = (stream_t *) p_this;
> + CHECK_FILE ( p_demux );
> +
> + if ( !stream_IsMimeType ( p_demux->s, "application/x-cue" )
> + && !stream_HasExtension( p_demux->s, ".cue" ))
> + return VLC_EGENERIC;
> +
> + p_demux->pf_control = access_vaDirectoryControlHelper;
> + p_demux->pf_readdir = ReadDir;
> + return VLC_SUCCESS;
> +}
> +
> +/**
> + * free should not be called on strings allocated by rust compiler.
> There is no
> + * way to ensure this without duplicating the string before passing it
> to
> + * input_item_t.
> + */
> +static char* rust_string_to_c_string(char *psz_rust) {
> + if (!psz_rust)
> + return NULL;
> + char *psz_c = strdup(psz_rust);
> + unref_cuesheet_string(psz_rust);
> + return psz_c;
> +}
> +
> +static char* get_cuesheet_property(CCuesheet *cuesheet, const char*
> psz_property) {
> + char* psz_val = cuesheet_get_property(cuesheet, psz_property);
> + return rust_string_to_c_string(psz_val);
> +}
> +
> +static char* get_cuesheet_comment(CCuesheet *cuesheet, const char*
> psz_property) {
> + char* psz_val = cuesheet_get_comment_value(cuesheet, psz_property);
> + return rust_string_to_c_string(psz_val);
> +}
> +
> +static char* get_track_property(CCuesheet *cuesheet, const char*
> psz_property, int track_num) {
> + char* psz_val = cuesheet_get_track_property(cuesheet, track_num,
> psz_property);
> + return rust_string_to_c_string(psz_val);
> +}
> +
> +static int ReadDir(stream_t* p_demux, input_item_node_t* p_subitems)
> +{
> + int i_ret = VLC_EGENERIC;
> + CCuesheet *cuesheet = cuesheet_from_demux(p_demux->s);
> + if(!cuesheet)
> + return i_ret;
> +
> + int tracks = cuesheet_get_tracks_number(cuesheet);
> + int idx = 0;
> + char *psz_val = NULL;
> + char *ppsz_option[2];
> +
> + char *psz_album = get_cuesheet_property(cuesheet, "TITLE");
> + char *psz_album_artist = get_cuesheet_property(cuesheet,
> "PERFORMER");
> + char *psz_description = get_cuesheet_comment(cuesheet, "COMMENT");
> + char *psz_genre = get_cuesheet_comment(cuesheet, "GENRE");
> + char *psz_date = get_cuesheet_comment(cuesheet, "DATE");
> +
> + char *psz_audio_file = get_cuesheet_property(cuesheet,
> "FILE_NAME");
> + if (!psz_audio_file)
> + goto end;
> +
> + char *psz_audio_file_uri = vlc_uri_encode(psz_audio_file);
> + char *psz_url = vlc_uri_resolve(p_demux->psz_url,
> psz_audio_file_uri);
> + free(psz_audio_file);
> + free(psz_audio_file_uri);
> +
> + while ( idx < tracks )
> + {
> + int i_options = 0;
> + input_item_t *p_item;
> +
> + psz_val = get_track_property(cuesheet, "TITLE", idx);
> + if (psz_val)
> + {
> + p_item = input_item_New(psz_url, psz_val);
> + p_item->i_type = ITEM_TYPE_FILE;
> + free(psz_val);
> + }
> + else
> + goto end;
> +
> + if (psz_album)
> + input_item_SetAlbum(p_item, psz_album);
> +
> + if (psz_album_artist)
> + input_item_SetAlbumArtist(p_item, psz_album_artist);
> +
> + if (psz_description)
> + input_item_SetDescription(p_item, psz_description);
> +
> + if (psz_date)
> + input_item_SetDate(p_item, psz_date);
> +
> + if (psz_genre)
> + input_item_SetGenre(p_item, psz_genre);
> +
> + psz_val = get_track_property(cuesheet, "PERFORMER", idx);
> + if (psz_val)
> + {
> + input_item_SetArtist(p_item, psz_val);
> + free(psz_val);
> + }
> +
> + int i_time = cuesheet_get_track_start(cuesheet, idx);
> + if (asprintf(ppsz_option, ":start-time=%d", i_time))
> + i_options++;
> +
> + if (idx < tracks - 1)
> + {
> + i_time = cuesheet_get_track_start(cuesheet, idx + 1);
> + if (asprintf(ppsz_option + i_options, ":stop-time=%d",
> i_time))
> + i_options++;
> + }
> +
> + input_item_AddOptions(p_item, i_options, (const char
> **)ppsz_option, VLC_INPUT_OPTION_TRUSTED);
> + input_item_node_AppendItem(p_subitems, p_item);
> + input_item_Release(p_item);
> + idx++;
> + }
> +
> + i_ret = VLC_SUCCESS;
> +
> + end:
> + free(psz_album);
> + free(psz_album_artist);
> + free(psz_description);
> + free(psz_genre);
> + free(psz_date);
> + unref_cuesheet(cuesheet);
> + return i_ret;
> +}
> +
> +vlc_module_begin()
> + add_shortcut( "playlist" )
> + set_category( CAT_INPUT )
> + set_subcategory( SUBCAT_INPUT_DEMUX )
> + set_description ( N_("Cue Sheet importer") )
> + set_capability ( "stream_filter", 320 )
> + set_callback ( Import_Cue_Sheet )
> +vlc_module_end()
> +
> +
> diff --git a/modules/demux/playlist/cuesheet/Cargo.toml
> b/modules/demux/playlist/cuesheet/Cargo.toml
> new file mode 100644
> index 0000000000..597076aa13
> --- /dev/null
> +++ b/modules/demux/playlist/cuesheet/Cargo.toml
> @@ -0,0 +1,13 @@
> +[package]
> +name = "cuesheet"
> +version = "0.1.0"
> +authors = ["Kartik Ohri <kartikohri13 at gmail.com>"]
> +edition = "2018"
> +license = "LGPL-2.1-or-later"
> +
> +[dependencies]
> +vlccore-rs = {path = "../../../../src/vlccore-rs"}
> +vlccore-sys = {path = "../../../../src/vlccore-sys"}
> +
> +[lib]
> +name = "cuesheet"
> \ No newline at end of file
> diff --git a/modules/demux/playlist/cuesheet/cbindgen.toml
> b/modules/demux/playlist/cuesheet/cbindgen.toml
> new file mode 100644
> index 0000000000..bc8fd14ae8
> --- /dev/null
> +++ b/modules/demux/playlist/cuesheet/cbindgen.toml
> @@ -0,0 +1,16 @@
> +header = "// SPDX-License-Identifier: LGPL-3.0-or-later"
Why this license? I don't think such license is acceptable in VLC (impossible to use on iOS for example).
> +sys_includes = ["stddef.h", "stdint.h", "stdlib.h"]
> +no_includes = true
> +include_guard = "CUESHEET_H"
> +tab_width = 4
> +language = "C"
> +documentation_style = "C"
> +style = "type"
> +cpp_compat = true
> +
> +[export]
> +item_types = ["enums", "structs", "unions", "typedefs", "opaque",
> "functions"]
> +
> +[enum]
> +rename_variants = "ScreamingSnakeCase"
> +prefix_with_name = true
> \ No newline at end of file
> diff --git a/modules/demux/playlist/cuesheet/src/capi.rs
> b/modules/demux/playlist/cuesheet/src/capi.rs
> new file mode 100644
> index 0000000000..cd8daac2ba
> --- /dev/null
> +++ b/modules/demux/playlist/cuesheet/src/capi.rs
> @@ -0,0 +1,122 @@
> +use crate::config::Cuesheet;
> +use std::convert::TryFrom;
> +use std::ffi::{CStr, CString};
> +use std::io::{BufRead, BufReader};
> +use std::mem::ManuallyDrop;
> +use std::os::raw::{c_char, c_int};
> +use std::ptr::null_mut;
> +
> +use vlccore_rs::stream;
> +use vlccore_sys::stream::stream_t;
> +
> +pub struct CCuesheet {
> + cuesheet: Cuesheet,
> +}
> +
> +#[no_mangle]
> +pub extern "C" fn cuesheet_from_demux(s: *mut stream_t) -> *mut
> CCuesheet {
> + let mut stream = s.into();
> + let ptr = rust_cuesheet_from_demux(&mut stream)
> + .map(|cuesheet| Box::new(CCuesheet { cuesheet }))
> + .map(|c_cuesheet| Box::into_raw(c_cuesheet))
> + .unwrap_or(null_mut());
> + let _ = ManuallyDrop::new(stream);
> + ptr
> +}
> +
> +fn rust_cuesheet_from_demux(stream: &mut stream::Stream) ->
> Option<Cuesheet> {
> + let mut cuesheet = Cuesheet::default();
> + let reader = BufReader::new(stream);
> + for line in reader.lines() {
> + match line {
> + Ok(l) => cuesheet.process_line(l.as_str())?,
> + Err(_) => return None,
> + }
> + }
> + Some(cuesheet)
> +}
> +
> +#[no_mangle]
> +pub unsafe extern "C" fn cuesheet_get_property(
> + c_cuesheet: *mut CCuesheet,
> + line: *const c_char,
> +) -> *mut c_char {
> + let cuesheet = &(*c_cuesheet).cuesheet;
> +
> + CStr::from_ptr(line)
> + .to_str()
> + .ok()
> + .and_then(|line| cuesheet.get_property(line))
> + .map(|property| convert_string_to_ptr(property.to_string()))
> + .unwrap_or(null_mut())
> +}
> +
> +#[no_mangle]
> +pub unsafe extern "C" fn cuesheet_get_comment_value(
> + c_cuesheet: *mut CCuesheet,
> + line: *const c_char,
> +) -> *mut c_char {
> + let cuesheet = &(*c_cuesheet).cuesheet;
> +
> + CStr::from_ptr(line)
> + .to_str()
> + .ok()
> + .and_then(|line| cuesheet.comments.get(line))
> + .map(|comment| convert_string_to_ptr(comment.to_string()))
> + .unwrap_or(null_mut())
> +}
> +
> +#[no_mangle]
> +pub unsafe extern "C" fn cuesheet_get_tracks_number(c_cuesheet: *mut
> CCuesheet) -> c_int {
> + (*c_cuesheet).cuesheet.tracks.len() as i32
> +}
> +
> +#[no_mangle]
> +pub unsafe extern "C" fn cuesheet_get_track_property(
> + c_cuesheet: *mut CCuesheet,
> + index: c_int,
> + line: *const c_char,
> +) -> *mut c_char {
> + let cuesheet = &(*c_cuesheet).cuesheet;
> +
> + usize::try_from(index)
> + .ok()
> + .and_then(|index| cuesheet.get_track_at_index(index))
> + .zip(CStr::from_ptr(line).to_str().ok())
> + .and_then(|(track, property)| track.get_property(property))
> + .map(|value| convert_string_to_ptr(value.to_string()))
> + .unwrap_or(null_mut())
> +}
> +
> +#[no_mangle]
> +pub unsafe extern "C" fn cuesheet_get_track_start(
> + c_cuesheet: *mut CCuesheet,
> + index: c_int,
> +) -> c_int {
> + let cuesheet = &(*c_cuesheet).cuesheet;
> +
> + usize::try_from(index)
> + .ok()
> + .and_then(|index| cuesheet.get_track_at_index(index))
> + .map(|track| track.begin_time_in_seconds())
> + .unwrap_or(-1)
> +}
> +
> +unsafe fn convert_string_to_ptr(string: String) -> *mut c_char {
> + if string.is_empty() {
> + return std::ptr::null_mut();
> + }
> + CString::new(string)
> + .map(|ptr| ptr.into_raw())
> + .unwrap_or(null_mut())
> +}
> +
> +#[no_mangle]
> +pub unsafe extern "C" fn unref_cuesheet_string(text: *mut c_char) {
> + let _ = CString::from_raw(text);
> +}
> +
> +#[no_mangle]
> +pub unsafe extern "C" fn unref_cuesheet(cuesheet: *mut CCuesheet) {
> + let _ = Box::from_raw(cuesheet);
> +}
> diff --git a/modules/demux/playlist/cuesheet/src/config.rs
> b/modules/demux/playlist/cuesheet/src/config.rs
> new file mode 100644
> index 0000000000..24ebbbc7a4
> --- /dev/null
> +++ b/modules/demux/playlist/cuesheet/src/config.rs
> @@ -0,0 +1,106 @@
> +use std::borrow::Borrow;
> +use std::collections::HashMap;
> +
> +#[derive(Clone, Debug)]
> +pub struct CuesheetTrack {
> + pub item_type: String,
> + pub item_position: u8,
> + pub item_performer: String,
> + pub item_title: String,
> + pub item_begin_duration: String,
> +}
> +
> +impl Default for CuesheetTrack {
> + fn default() -> Self {
> + CuesheetTrack {
> + item_type: "".to_owned(),
> + item_position: 0,
> + item_performer: "".to_string(),
> + item_title: "".to_string(),
> + item_begin_duration: "".to_string(),
> + }
> + }
> +}
> +
> +fn add_to_start_time(
> + start_time: i32,
> + time_to_add: Option<&str>,
> + conversion_factor: i32,
> +) -> Option<i32> {
> + let val = time_to_add?.parse::<u32>().ok()?;
> + Some(start_time + val as i32 * conversion_factor)
> +}
> +
> +impl CuesheetTrack {
> + pub fn begin_time_in_seconds(&self) -> i32 {
> + let start: &str = &self.item_begin_duration;
> + let mut duration_parts: Vec<&str> = start.split(":").collect();
> +
> + let frame_rate = 75;
> + let convert_min_to_s = 60;
> +
> + duration_parts
> + .pop()
> + .and_then(|part| part.parse::<u32>().ok())
> + .map(|part| part / frame_rate)
> + .and_then(|start| add_to_start_time(start as i32,
> duration_parts.pop(), 1))
> + .and_then(|start| {
> + add_to_start_time(start as i32, duration_parts.pop(),
> convert_min_to_s)
> + })
> + .unwrap_or(-1)
> + }
> +
> + pub fn get_property(&self, key: &str) -> Option<&str> {
> + match key.to_ascii_lowercase().as_str() {
> + "type" => Some(self.item_type.as_str()),
> + "begin_duration" =>
> Some(self.item_begin_duration.as_str()),
> + "performer" => Some(self.item_performer.as_str()),
> + "title" => Some(self.item_title.as_str()),
> + _ => None,
> + }
> + }
> +}
> +
> +#[derive(Clone, Debug)]
> +pub struct Cuesheet {
> + pub file_name: String,
> + pub file_type: String,
> + pub tracks: Vec<CuesheetTrack>,
> + pub performer: String,
> + pub title: String,
> + pub comments: HashMap<String, String>,
> + pub is_processing_tracks: bool,
> +}
> +
> +impl Default for Cuesheet {
> + fn default() -> Self {
> + Cuesheet {
> + file_name: "".to_string(),
> + file_type: "".to_string(),
> + tracks: Vec::new(),
> + performer: "".to_string(),
> + title: "".to_string(),
> + comments: HashMap::new(),
> + is_processing_tracks: false,
> + }
> + }
> +}
> +
> +impl Cuesheet {
> + pub fn get_track_at_index(&self, index: usize) ->
> Option<&CuesheetTrack> {
> + if index >= self.tracks.len() {
> + return None;
> + }
> + Some(self.tracks[index].borrow())
> + }
> +
> + pub fn get_property(&self, key: &str) -> Option<&str> {
> + match key.to_ascii_lowercase().as_str() {
> + "file_name" => Some(self.file_name.as_str()),
> + "file_type" => Some(self.file_type.as_str()),
> + "performer" => Some(self.performer.as_str()),
> + "title" => Some(self.title.as_str()),
> + _ => None,
> + }
> + }
> +}
> diff --git a/modules/demux/playlist/cuesheet/src/lib.rs
> b/modules/demux/playlist/cuesheet/src/lib.rs
> new file mode 100644
> index 0000000000..c43541e34f
> --- /dev/null
> +++ b/modules/demux/playlist/cuesheet/src/lib.rs
> @@ -0,0 +1,5 @@
> +mod config;
> +mod parse;
> +
> +#[cfg(cargo_c)]
> +mod capi;
> diff --git a/modules/demux/playlist/cuesheet/src/parse.rs
> b/modules/demux/playlist/cuesheet/src/parse.rs
> new file mode 100644
> index 0000000000..b570638325
> --- /dev/null
> +++ b/modules/demux/playlist/cuesheet/src/parse.rs
> @@ -0,0 +1,75 @@
> +use crate::config::*;
> +
> +pub fn parse_line(line: &str) -> Vec<String> {
> + let characters = line.trim().chars();
> + let mut parts: Vec<String> = Vec::new();
> + let mut inside_quotes: bool = false;
> + let mut temp_str: String = String::new();
> + for char in characters {
> + if char == '"' {
> + inside_quotes = !inside_quotes;
> + } else if inside_quotes {
> + temp_str.push(char);
> + } else if char.is_whitespace() {
> + parts.push(temp_str.clone());
> + temp_str.clear();
> + } else {
> + temp_str.push(char);
> + }
> + }
> + parts.push(temp_str.clone());
> + parts
> +}
> +
> +impl Cuesheet {
> + pub fn process_line(&mut self, line: &str) -> Option<()> {
> + let parts = parse_line(line);
> + let keyword = parts[0].as_str();
> + if !self.is_processing_tracks {
> + match keyword {
> + "FILE" => {
> + self.file_name = parts[1].to_owned();
> + self.file_type = parts[2].to_owned();
> + }
> + "TITLE" => self.title = parts[1].to_owned(),
> + "PERFORMER" => self.performer = parts[1].to_owned(),
> + "REM" => {
> + self.comments
> + .insert(parts[1].to_owned().to_lowercase(),
> parts[2].to_owned());
> + }
> + "TRACK" => self.is_processing_tracks = true,
> + _ => {}
> + };
> + }
> + if self.is_processing_tracks {
> + if keyword == "TRACK" {
> + let mut track = CuesheetTrack::default();
> + track.item_position =
> parts[1].to_owned().parse::<u8>().ok()?;
> + track.item_type = parts[2].to_owned();
> + self.tracks.push(track);
> + } else {
> + self.tracks.last_mut()?.process_track(line);
> + }
> + }
> + Some(())
> + }
> +}
> +
> +impl CuesheetTrack {
> + fn process_track(&mut self, line: &str) -> Option<()> {
> + let parts = parse_line(line);
> + let keyword = parts[0].as_str();
> + match keyword {
> + "TITLE" => self.item_title = parts[1].to_owned(),
> + "PERFORMER" => self.item_performer = parts[1].to_owned(),
> + "INDEX" => {
> + let index_position =
> parts[1].to_owned().parse::<u8>().ok()?;
> + if index_position == 1 {
> + self.item_begin_duration = parts[2].to_owned();
> + }
> + }
> + _ => {}
> + };
> + Some(())
> + }
> +}
> --
> 2.25.1
>
> _______________________________________________
> vlc-devel mailing list
> To unsubscribe or modify your subscription options:
> https://mailman.videolan.org/listinfo/vlc-devel
More information about the vlc-devel
mailing list