Skip to content

New syntax to CLI "play" command and "watch_continuously" config #667

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 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
108 changes: 88 additions & 20 deletions trackma/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#

import datetime
import itertools
import os
import random
import re
Expand All @@ -23,6 +24,7 @@
import sys
import time
from decimal import Decimal
from typing import Iterable

from trackma import data
from trackma import messenger
Expand Down Expand Up @@ -912,56 +914,122 @@ def play_random(self):
show = random.choice(newep)
return self.play_episode(show)

def play_episode(self, show, playep=0):
def play_episode(self, show, playep : int | str | Iterable[int] = 0):
"""
Does a local search in the hard disk (in the folder specified by the config file)
for the specified episode (**playep**) for the specified **show**.

If no **playep** is specified, the next episode of the show will be returned.
If no **playep** is zero or not specified, the next episode of the show will be returned.

**playep** may also be a iterable of episode numbers, like `range(3, 12)`
"""
# Check if operation is supported by the API
if not self.mediainfo.get('can_play'):
raise utils.EngineError(
'Operation not supported by current site or mediatype.')

try:
playep = int(playep)
except ValueError:
raise utils.EngineError('Episode must be numeric.')

if not show:
raise utils.EngineError('Show given is invalid')

if playep <= 0:
playep = show['my_progress'] + 1
if isinstance(playep, int) or isinstance(playep, str):
try:
first_ep = int(playep)
except ValueError:
raise utils.EngineError('Episode must be a integer.')

if first_ep <= 0:
first_ep = show['my_progress'] + 1

if self.config['watch_continuously']:
ep_iter = itertools.count(first_ep)
next(ep_iter) # pop first_ep
else:
ep_iter = ()

elif isinstance(playep, Iterable):
ep_iter = iter(playep)
try:
first_ep = next(ep_iter)
except StopIteration:
raise utils.EngineError('Range of episodes is empty.')

self.msg.info(
f"Searching episode '{first_ep}' of '{show['title']}' from library...")

if show['total'] and playep > show['total']:
if show['total'] > 0 and first_ep > show['total']:
raise utils.EngineError('Episode beyond limits.')

self.msg.info("Getting '%s' episode '%s' from library..." %
(show['title'], playep))
ep_iter = itertools.chain((first_ep,), ep_iter)
filenames = []
ep_numbers = []

try:
filename = self.get_episode_path(show, playep)
for ep in ep_iter:
filename = self.get_episode_path(show, ep)
filenames.append(filename)
ep_numbers.append(ep)
except utils.EngineError:
self.msg.info("Episode not found. Calling hooks...")
self._emit_signal("episode_missing", show, playep)
return []

self.msg.info('Found. Starting player...')
if not filenames:
self.msg.info("Episode not found. Calling hooks...")
self._emit_signal("episode_missing", show, first_ep)
return []

self.msg.info('Found. Playing episodes: ' + repr(ep_numbers))
args = shlex.split(self.config['player'])

if not args:
raise utils.EngineError('Player not set up, check your config.json')

args[0] = shutil.which(args[0])
args[0] = shutil.which(args[0]) or ""

if not args[0]:
raise utils.EngineError('Player not found, check your config.json')

args.append(filename)
args.extend(filenames)
return args

def parse_episode_range(self, show, raw_range: str) -> Iterable[int]:
"""
Parse a string into a iterable of episode numbers, acording to a custom syntax.
"""
def parse_num(num_str):
if num_str.startswith("n"):
return int(num_str.lstrip("n")) + show["my_progress"]
else:
return int(num_str)

if not raw_range:
raw_range = "n1-" if self.config['watch_continuously'] else "-n1"
elif raw_range == "n0":
raw_range = "n0-n0"

# convert relative "nX" to "-nX"
if "-" not in raw_range and raw_range.startswith("n"):
raw_range = "-" + raw_range

# absolute "X"
if "-" not in raw_range:
first = int(raw_range)

if self.config['watch_continuously']:
ep_iter = itertools.count(first)
else:
ep_iter = (first, )
else:
first, last = raw_range.split("-")
# a left-open range always expand to n1
first = parse_num(first or "n1")

if last:
ep_iter = range(first, 1 + parse_num(last))
else:
ep_iter = itertools.count(first)

if first == 0:
raise utils.EngineError("0 is a invalid episode number")

return ep_iter

def undoall(self):
"""Clears the data handler queue and discards any unsynced change."""
return self.data_handler.queue_clear()
Expand Down
37 changes: 27 additions & 10 deletions trackma/ui/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -503,24 +503,41 @@ def do_tracker(self, args):

def do_play(self, args):
"""
Starts the media player with the specified episode number (next if unspecified).
Starts the media player with the specified episode range.
Also, check the "watch_continuously" config option.
Examples of useful episode ranges: '', '-', 'n1', 'n3', '3', '3-', '-6'
_
Episode range syntax: 'X', 'X-Y', 'X-', '-X'.
'X' is absolute ep number: '2' is the second ep.
'nX' is relative to the last seen ep: 'n1' is the next ep unwatched.
'X-Y' will open 'X' until 'Y' (inclusive): '2-6' ep 2, until ep 6.
'X-' will open 'X' and every ep after: '3-' start ep 3 onwards.
'-X' will begin at 'n1': '-6' last ep, until ep 6.
_
For convenience, some conversions are made:
'-' -> 'n1-' - continue watching onwards
'' -> '-n1' -> 'n1-n1' - watch next episode
'n4' -> '-n4' -> 'n1-n4' - watch next 4 episodes
'3' -> '3-3' - watch episode 3
If "watch_continuously" config is "true", this changes:
'' -> 'n1-' - continue watching onwards
'6' -> '6-' - play episode 6 onwards

:param show Episode index or title.
:optparam ep Episode number. Assume next if not specified.
:usage play <show index or title> [episode number]
:optparam ep Episode range. See syntax above.

:usage play <show index or title> [episode range]
"""
try:
episode = 0
show = self._get_show(args[0])

# If the user specified an episode, play it
# If the user specified an episode range, play it
# otherwise play the next episode not watched yet
if len(args) > 1:
episode = args[1]

args = self.engine.play_episode(show, episode)
raw_range = args[1] if len(args) > 1 else ""
ep_iter = self.engine.parse_episode_range(show, raw_range)
args = self.engine.play_episode(show, ep_iter)
utils.spawn_process(args)
except utils.TrackmaError as e:
except (utils.TrackmaError, ValueError) as e:
self.display_error(e)

def do_openfolder(self, args):
Expand Down
29 changes: 13 additions & 16 deletions trackma/ui/curses.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,11 +357,8 @@ def do_update(self):
self.update_request, show['my_progress']+1)

def do_play(self):
item = self._get_selected_item()
if item:
show = self.engine.get_show_info(item.showid)
self.ask('[Play] Episode # to play: ',
self.play_request, show['my_progress']+1)
if self._get_selected_item():
self.ask('[Play] Episode range to play (CLI syntax): ', self.play_request, "")

def do_openfolder(self):
item = self._get_selected_item()
Expand Down Expand Up @@ -414,7 +411,7 @@ def do_help(self):
helptext += "https://github.com/z411/trackma\n\n"
helptext += "This program is licensed under the GPLv3,\nfor more information read COPYING file.\n\n"
helptext += "More controls:\n {prev_filter}/{next_filter}:Change Filter\n {search}:Search\n {addsearch}:Add\n {reload}:Change API/Mediatype\n"
helptext += " {delete}:Delete\n {send}:Send changes\n {sort_order}:Change sort order\n {retrieve}:Retrieve list\n {details}: View details\n {open_web}: Open website\n {openfolder}: Open folder containing show\n {altname}:Set alternative title\n {neweps}:Search for new episodes\n {play_random}:Play Random\n {switch_account}: Change account"
helptext += " {delete}:Delete\n {send}:Send changes\n {sort_order}:Change sort order\n {retrieve}:Retrieve list\n {details}: View details\n {open_web}: Open website\n {openfolder}: Open folder containing show\n {altname}:Set alternative title\n {neweps}:Search for new episodes\n {play}:Play episode or range (see syntax in CLI interface)\n {play_random}:Play Random\n {switch_account}: Change account"
helptext = helptext.format(**self.keymap_str)
ok_button = urwid.Button('OK', self.help_close)
ok_button_wrap = urwid.Padding(urwid.AttrMap(
Expand Down Expand Up @@ -646,18 +643,18 @@ def altname_request(self, data):
self.error(e)
return

def play_request(self, data):
def play_request(self, raw_range):
self.ask_finish(self.play_request)
if data:
item = self._get_selected_item()
show = self.engine.get_show_info(item.showid)
item = self._get_selected_item()
show = self.engine.get_show_info(item.showid)

try:
args = self.engine.play_episode(show, data)
utils.spawn_process(args)
except utils.TrackmaError as e:
self.error(e)
return
try:
ep_iter = self.engine.parse_episode_range(show, raw_range)
args = self.engine.play_episode(show, ep_iter)
utils.spawn_process(args)
except (utils.TrackmaError, ValueError) as e:
self.error(e)
return

def prompt_update_request(self, data):
(show, episode) = self.last_update_prompt
Expand Down
2 changes: 1 addition & 1 deletion trackma/ui/gtk/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,7 @@ def _on_show_action(self, main_view, event_type, data):
def _play_next(self, show_id):
show = self._engine.get_show_info(show_id)
try:
args = self._engine.play_episode(show)
args = self._engine.play_episode(show, 0)
utils.spawn_process(args)
except utils.TrackmaError as e:
self._error_dialog(e)
Expand Down
1 change: 1 addition & 0 deletions trackma/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,7 @@ class APIFatal(TrackmaFatal):
'autosend_at_exit': True,
'library_autoscan': True,
'library_full_path': False,
'watch_continuously': False,
'scan_whole_list': False,
'debug_disable_lock': True,
'auto_status_change': True,
Expand Down