Skip to content

Commit

Permalink
feat(ytdl-mpv)!: add initial support to track chapters (#29)
Browse files Browse the repository at this point in the history
* feat(ytdl-mpv)!: add initial support to track chapters

browse chapter list and jump to a particular one

**breaking changes:**
 * two new required deps added: `jq` and `iconv`
 * new cols inside cache DB so a flush is needed!

* chore(bin/ytdl-mpv): add new keyb to edit menu

explore track chapters using "Alt+Enter"
if available, otherwise play as usual

* feat(ytdl-mpv): no hard-coded sleep when switching

between chapters of different tracks, await until switch
with a safer counter for loop break

* feat(README): update required deps, gif demo and feats
  • Loading branch information
andros21 authored Nov 11, 2023
1 parent 6b5337d commit e5d6d4b
Show file tree
Hide file tree
Showing 3 changed files with 255 additions and 81 deletions.
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,28 @@
<a href="#runner-run">Run</a>
</div>
<br>
<img src="https://user-images.githubusercontent.com/58751603/191463905-c7154c4b-9f4c-460b-85c1-7c5cdbb74d1c.gif" alt="Demo" width="570">
<img src="https://github.com/andros21/ytdl-mpv/assets/58751603/62ea0632-0b6a-4975-a23d-870e98dfa6de" alt="Demo" width="570">
</div>

## :star: Features

- Search history and yt query cache
- Customize script using available flags
- Simple and intuitive rofi menu
- Play video, audio
- Append to playlist
- Play video or audio [`Alt+v`]
- Append to playlist [`Alt+Tab`]()
- Loop playlist
- Edit current playlist
- Edit current playlist [`Alt+r`]()
- Save/Load current playlist
- Help for key bindings
- Help for key bindings [`Alt+h`]
- Jump to chapters [`Alt+Enter`]()
- ...

## :rocket: Setup

**Dependencies**

- `socat` `sqlite3` `xargs`
- `socat` `sqlite3` `xargs` `jq` `iconv`
- [`mpv>0.35.1`](https://github.com/mpv-player/mpv)
- [`rofi>=1.6.1`](https://github.com/davatorium/rofi)
- [`yt-dlp==2023.10.13`](https://github.com/yt-dlp/yt-dlp)
Expand All @@ -70,5 +71,5 @@ make install

## :runner: Run

Is advisable to key bind `ytdl-mpv`, so that rofi menu can be opened wherever you are!\
Is advisable to key bind (e.g. [`Cmd+m`]()) `ytdl-mpv`, so that rofi menu can be opened wherever you are!\
For **debugging purposes**, instead, could be useful run `ytdl-mpv` inside a shell to view stdout/stderr
195 changes: 150 additions & 45 deletions bin/mpvctl
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,13 @@ _die() {

# Ensure dependencies
_checkDep() {
type mpv > /dev/null 2>&1 || _die "Cannot find mpv in your \$PATH"
type yt-dlp > /dev/null 2>&1 || _die "Cannot find yt-dlp in your \$PATH"
type socat > /dev/null 2>&1 || _die "Cannot find socat in your \$PATH"
local deps
deps=(mpv socat yt-dlp jq iconv)
for dep in "${deps[@]}"; do
type "$dep" > /dev/null 2>&1 || _die "Cannot find ${dep} in your \$PATH"
done
(echo "D̲eep̲ ̲P̲u̲r̲ple" | iconv -f utf-8 -t ascii//translit &>/dev/null) \
|| _die "Unsupported iconv flavour, install gnu-libiconv"
SOCKCMD="socat - $SOCKET"
}

Expand All @@ -47,20 +51,24 @@ usage: $(basename "$0") [-h] [--socket SOCKET] action
MPVCTL - mpv cli ipc-json frontend
positional arguments:
add add items to playlist
add add tracks to playlist
check check socket status
clear playlist clear
load load a playlist from given path
loop loop/unloop currently playing playlist
loop-status get loop status currently playing playlist
next play next item in playlist
next play next track (or chapter) in playlist
playlist print sorted list of tracks
prev play prev item in playlist
rm remove item number from playlist
prev play prev track (or chapter) in playlist
rm remove track number from playlist
save save current playlist to given path
stop always stop playback
toggle toggle playback
track jump to playlist item number
track jump to playlist track number
ctrack get current track playlist number
chapters get chapter list (if available)
chapter jump to track chapter number
cchapter get current chapter (-1 if no chapters)
optional arguments:
-h, --help print this help
Expand Down Expand Up @@ -89,10 +97,10 @@ _getPlaylist() {
_checkSock
# track numbers
local trnum
trnum=$(_getProperty 'get_property_string' 'playlist-count')
trnum=$(_getProperty 'playlist-count')
# current track number
local trcur
trcur=$(_getProperty 'get_property_string' 'playlist-pos')
trcur=$(_getProperty 'playlist-pos')

local count
count=0
Expand All @@ -103,38 +111,40 @@ _getPlaylist() {
# track name
local trname
local trcurmark
trname=$(_getProperty 'get_property_string' "playlist/$count/filename")
trname=$(_getProperty "playlist/$count/filename")
if [ "$count" -eq "$trcur" ]; then trcurmark='*'; fi
# check if local or yt media
if [[ $trname =~ ^ytdl://(.*)$ ]]; then
# track id
local trid
trid=${trname:7}
# escape quotes
trid=$(printf '%s' "$trid" | sed "s/'/''/g")
# if cache db given, search yt media using it
if [ -n "$1" ] && [ -f "$1" ]; then
local db
db=$1;
# search for track title
local trtitle
trtitle="$(sqlite3 "${db}" \
"select distinct title from main where id='${trid}'" 2> /dev/null)"
"select distinct nchapters,title from main where id='${trid}'")"
if [ -z "$trtitle" ]; then
local trtitle
trtitle=$(yt-dlp --get-filename "$trid" -o "%(title)s" 2> /dev/null)
local csv
csv=$(yt-dlp --dump-json "https://youtube.com/watch?v=${trid}" \
| sed 's/\xE2\x80\x9D//g;s/\xE2\x80\x9C//g' \
| jq -r '. | {query:"NULL",id:.id,title:.title,nchapters:(.chapters|length),chapters:(.chapters|if .==null then [] else [.[].title] end)} | map(tostring) | @csv' \
| iconv -f utf-8 -t ascii//translit
)
# cache this single search as relative to a NULL query
sqlite3 "${db}" \
"insert into main (query,id,title) values ('NULL','${trid}','${trtitle}')" \
2> /dev/null
sqlite3 "${db}" "insert into main (query,id,title,nchapters,chapters) values (${csv})"
trtitle="$(sqlite3 "${db}" "select nchapters,distinct title from main where id='${trid}'")"
fi
else
# searching track title, using ytdl
local trtitle
trtitle=$(yt-dlp --get-filename "$trid" -o "%(title)s" 2> /dev/null)
trtitle=$(yt-dlp --get-title "https://youtube.com/watch?v=${trid}" \
| iconv -f utf-8 -t ascii//translit)
fi
if [ -z "$trtitle" ]; then
printf "[Warning] yt-dlp title search fail\n" >&2
printf '[Warning] yt-dlp title search failed for %s\n' "$trid" >&2
local trtitle
trtitle="NULL"
fi
Expand All @@ -145,26 +155,131 @@ _getPlaylist() {
fi

local zerocount
zerocount=$(printf '%s\n' "$((count+1))" | sed 's/\<[0-9]\>/0&/')
printf '%s)%s %s\n' "$zerocount" "$trcurmark" "$trtitle"
zerocount=$(printf '%s\n' "$((count+1))")
printf '%s)%s %s\n' "$zerocount" "$trcurmark" "$trtitle" \
| sed 's/ 0|/ [##] /;s/ \([0-9]\+\)|/ [\1] /' \
| sed 's/^[0-9])/0&/;s/\[\([0-9]\)\]/[0\1]/'
count=$((count + 1))
done
}

# get chapters from mpv metadata
_getChaptersMpv() {
local nch
# number of chapters
nch=$(_getProperty "chapters")
if [ "$nch" -gt "0" ]; then
local chcur
# get current chapter
chcur=$(_getProperty 'chapter')
chcur=$((chcur+1))
# get chapters list from mpv player
_getProperty 'chapter-list' \
| jq -r '.[].title' \
| awk '{ print FNR ") " $0 }' \
| sed "s/\(^$chcur)\) /\1*/" \
| sed 's/^[0-9])/0&/'
fi
}
#
# get chapters from metadata inside sqlite3 db
_getChaptersDb() {
# sqlite db
local db
db=$2
# track to inspect id
local trid
trid=$(_getProperty "playlist/$trnum/filename")
trid=${trid:7}
# number of chapters
local nch
nch="$(sqlite3 "${db}" "select nchapters from main where id='${trid}'")"
if [ "$nch" -gt "0" ]; then
# get chapters list from sqlite3 db
sqlite3 "$db" "select chapters from main where id='${trid}'" \
| jq -r '.[]' \
| awk '{ print FNR ") " $0 }' \
| sed 's/^[0-9])/0&/'
fi
}

# Get track chapters if available
_getChapters() {
# track number to inspect
local trnum
trnum=$1
[ -z "$trnum" ] && _die "Missing parameter: track number in playlist"
# current track number
local trcur
trcur=$(_getProperty 'playlist-pos')
if [ "$trnum" -eq "$trcur" ]; then
_getChaptersMpv
else
# if cache db given, search track chapters using it
if [ -n "$2" ] && [ -f "$2" ]; then
_getChaptersDb "$trnum" "$2"
fi
fi
}

# next track in playlist unless chapters detected
# in this case it is preferable to play next chapter
_nextPlaylist() {
local nch
# number of chapters
nch=$(_getProperty "chapters")
if [ "$nch" -gt "0" ]; then
local chcur
# get current chapter
chcur=$(_getProperty 'chapter')
# if at the last chapter play next track
# else next chapter
if [ "$chcur" -eq "$((nch-1))" ]; then
_setProperty 'playlist_next'
else
_setProperty 'set_property' 'chapter' "$((chcur+1))"
fi
else
_setProperty 'playlist_next'
fi
}

# previous track in playlist unless chapters detected
# in this case it is preferable to play previous chapter
_prevPlaylist() {
local nch
# number of chapters
nch=$(_getProperty "chapters")
if [ "$nch" -gt "0" ]; then
local chcur
# get current chapter
chcur=$(_getProperty 'chapter')
# if at the first chapter play previous track
# else previous chapter
if [ "$chcur" -eq "0" ]; then
_setProperty 'playlist_prev'
else
_setProperty 'set_property' 'chapter' "$((chcur-1))"
fi
else
_setProperty 'playlist_prev'
fi
}

# Save current playlist to given file
_savePlaylist() {
_checkSock
[[ -n "$1" ]] || _die 'None path given'
[[ -d "$(dirname "$1")" ]] || _die 'Invalid path given'
# track numbers
local trnum
trnum=$(_getProperty 'get_property_string' 'playlist-count')
trnum=$(_getProperty 'playlist-count')
local count
count=0
while [ "$count" -lt "$trnum" ]; do
# track name
local trname
trname=$(_getProperty 'get_property_string' "playlist/$count/filename")
trname=$(_getProperty "playlist/$count/filename")
printf '%s\n' "$trname"
count=$((count + 1))
done > "$1"
Expand All @@ -183,8 +298,7 @@ _loadPlaylist() {
_getLoop() {
#loop state
local lstate
lstate=$(_getProperty 'get_property_string' 'loop-playlist' \
| sed "s/inf/on/" | sed "s/no/off/")
lstate=$(_getProperty 'loop-playlist' | sed "s/inf/on/" | sed "s/no/off/")
if [ -z "$lstate" ]; then
exit 1
fi
Expand All @@ -208,28 +322,15 @@ _toggleLoop() {
# Get method to read from socket
_getProperty() {
_checkSock
local tosend
tosend='{ "command": ['
for arg in "$@"; do
tosend="$tosend \"$arg\","
done
tosend=${tosend%?}' ] }'
local property
property=$(printf '%s\n' "$tosend" | $SOCKCMD 2> /dev/null \
| cut -d'"' -f 4 | rev | cut -d'.' -f 2- | rev)
property=$(printf '{"command":["get_property_string","%s"]}\n' "$1" | $SOCKCMD | jq -r .data)
printf '%s\n' "$property"
}

# Set method to write from socket
_setProperty() {
_checkSock
local tosend
tosend='{ "command": ['
for arg in "$@"; do
tosend="$tosend \"$arg\","
done
tosend=${tosend%?}' ] }'
printf '%s\n' "$tosend" | $SOCKCMD &> /dev/null
printf '%s\n' "$@" | jq -R . | jq -c -s '{command:.}' | $SOCKCMD &> /dev/null
}


Expand All @@ -249,14 +350,18 @@ case "$1" in
load) _loadPlaylist "$2" ;;
loop) _toggleLoop ;;
loop-status) _getLoop ;;
next) _setProperty 'playlist_next' ;;
playlist) _getPlaylist "$2" "$3" ;;
prev) _setProperty 'playlist_prev' ;;
next) _nextPlaylist ;;
playlist) _getPlaylist "$2" ;;
prev) _prevPlaylist ;;
rm) _setProperty 'playlist_remove' "$2" ;;
save) _savePlaylist "$2" ;;
stop) _setProperty 'quit' ;;
toggle) _setProperty 'cycle' 'pause' ;;
track) _setProperty 'set_property' 'playlist-pos' "$2" ;;
ctrack) _getProperty 'playlist-pos' ;;
chapters) _getChapters "$2" "$3" ;;
chapter) _setProperty 'set_property' 'chapter' "$2" ;;
cchapter) _getProperty 'chapter' | sed 's/null/-1/' ;;
-h | --help) shift; _usage; exit 0 ;;
*) shift; _usage; exit 1 ;;
esac
Loading

0 comments on commit e5d6d4b

Please sign in to comment.