8 use Encode qw{encode_utf8 decode_utf8};
12 binmode STDIN, ":utf8";
13 binmode STDOUT, ":utf8";
14 binmode STDERR, ":utf8";
17 (my $prog = $0) =~ s:^.*/::;
18 my %CMD_HELP; my %CMD_FN;
22 $help =~ /^(\S+)/ or die "bad usage";
24 $CMD_HELP{$cmd} = $help;
27 sub HELP_MESSAGE ($;@) {
30 print $fh "usage: $prog $CMD_HELP{$CMD}\n";
32 print $fh "usage: $prog CMD ARGS ...\n";
33 for my $cmd (sort keys %CMD_HELP) { print $fh "\t$CMD_HELP{$cmd}\n"; }
36 sub bail_usage () { HELP_MESSAGE \*STDERR; exit 2; }
39 my $ROOT = "/mnt/dvd/archive";
42 my ($opts) = @_; $opts //= {};
43 my %opts = (AutoCommit => 0,
47 $DB = DBI->connect("dbi:Pg:host=roadstar", "", "", \%opts);
50 sub must_exist ($$@) {
51 my ($msg, $query, @args) = @_;
52 my $st = $DB->prepare($query);
54 my ($n) = $st->fetchrow_array; $st->finish;
59 must_exist("unknown group `$group'",
60 "SELECT COUNT(*) FROM playlist_group WHERE name = ?", $group);
64 must_exist("unknown list `$list'",
65 "SELECT COUNT(*) FROM playlist WHERE name = ?", $list);
67 sub must_member ($$) {
68 my ($group, $list) = @_;
70 must_exist("`$list' is not a member of `$group'",
71 "SELECT COUNT(*) FROM playlist_position
72 WHERE group_name = ? AND list_name = ?",
76 defcmd "newgroup NAME", sub {
77 @_ == 1 or bail_usage;
79 db_connect { ReadOnly => 0 };
80 $DB->do("INSERT INTO playlist_group (name) VALUES (?)", undef, $name);
83 defcmd "rmgroup NAME", sub {
84 @_ == 1 or bail_usage;
86 db_connect { ReadOnly => 0 };
88 $DB->do("DELETE FROM playlist_group WHERE name = ?", undef, $name);
91 defcmd "groups", sub {
92 @_ == 0 or bail_usage;
94 my $st = $DB->prepare("SELECT name FROM playlist_group ORDER BY name");
96 while (my @r = $st->fetchrow_array) { my ($name) = @r; print "$name\n"; }
100 @_ == 0 or bail_usage;
102 my $st = $DB->prepare("SELECT name FROM playlist ORDER BY name");
104 while (my @r = $st->fetchrow_array) { my ($name) = @r; print "$name\n"; }
107 defcmd "edit GROUP LIST[=[+|-]POS]|-LIST ...", sub {
108 @_ >= 2 or bail_usage;
109 my ($group, @ops) = @_;
111 db_connect { ReadOnly => 0 };
113 my $st_chk = $DB->prepare("SELECT n_entry FROM playlist WHERE name = ?");
114 my $st_getpos = $DB->prepare
115 ("SELECT next_entry FROM playlist_position
116 WHERE group_name = ? AND list_name = ?");
117 my $st_set = $DB->prepare
118 ("INSERT INTO playlist_position AS p (group_name, list_name, next_entry)
120 ON CONFLICT (group_name, list_name)
121 DO UPDATE SET next_entry = ?
122 WHERE p.group_name = ? AND p.list_name = ?");
123 my $st_del = $DB->prepare
124 ("DELETE FROM playlist_position WHERE group_name = ? AND list_name = ?");
127 if ($op =~ /^ - (.++) $/x) {
129 must_list $list; must_member $group, $list;
130 $st_del->execute($group, $list);
131 } elsif ($op =~ /^ ([^=]++) (?: = ([-+])?+ (\d++))?+ $/x) {
132 my ($list, $rel, $pos) = ($1, $2, $3);
134 $st_chk->execute($list);
135 my @r = $st_chk->fetchrow_array; @r or die "unknown list `$list'";
136 my ($n) = @r; $st_chk->finish;
140 $st_getpos->execute($group, $list);
141 my @r = $st_getpos->fetchrow_array;
142 @r or die "`$list' is not a member of `$group'";
143 my ($cur) = @r; $st_getpos->finish;
145 if ($rel eq "+") { $pos = $cur + $pos; }
146 elsif ($rel eq "-") { $pos = $cur - $pos; }
148 0 <= $pos && $pos < $n
149 or die "`$list' position $pos out of range 0 .. $n";
150 $st_set->execute($group, $list, $pos,
151 $pos, $group, $list);
153 die "bad edit op `$op'";
158 defcmd "next [-pu] [-o N] [-n N] GROUP [LIST]", sub {
162 getopts("o:n:pu", \%opt) or $bogusp = 1;
163 1 <= @ARGV && @ARGV <= 2 or $bogusp = 1;
164 !defined $opt{"o"} || $opt{"o"} =~ /^[-+]?\d+$/ or $bogusp = 1;
165 !defined $opt{"n"} || $opt{"n"} =~ /^\d+$/ or $bogusp = 1;
166 my $o = $opt{"o"} // 0;
167 my $n = $opt{"n"} // 1;
168 if ($bogusp) { bail_usage; }
169 my ($group, $list) = @ARGV;
173 defined $list and must_member $group, $list;
176 if (!defined $list) {
177 my $st = $DB->prepare
178 ("SELECT MAX(LENGTH(list_name))
179 FROM playlist_position
180 WHERE group_name = ?");
181 $st->execute($group); ($glen) = $st->fetchrow_array; $st->finish;
184 my %elen; my $elen_max = -1;
187 "SELECT p.list_name, MAX(e.entry)
188 FROM playlist_entry AS e
189 JOIN playlist_position AS p ON e.list_name = p.list_name
190 WHERE p.group_name = ? AND
191 p.next_entry + ? <= e.entry AND e.entry < p.next_entry + ?";
192 my @arg = ($group, $o, $n + $o);
193 if (defined $list) { $q .= " AND p.list_name = ?"; push @arg, $list; }
194 $q .= " GROUP BY p.list_name";
195 my $st = $DB->prepare($q); $st->execute(@arg);
196 while (my @r = $st->fetchrow_array) {
198 my $elen = $elen{$l} = defined $emax ? length $emax : -1;
199 $elen > $elen_max and $elen_max = $elen;
204 "SELECT p.list_name, p.next_entry, e.entry, c.n, s.title, m.title,
205 m.path, m.title_number, m.start_chapter, m.end_chapter
206 FROM playlist_position AS p
207 JOIN playlist_entry AS e ON p.list_name = e.list_name
208 JOIN media AS m ON e.media_id = m.id
209 JOIN series AS s ON m.series_name = s.name
210 JOIN (SELECT t.list_name, COUNT(*) AS n
211 FROM (SELECT DISTINCT e.list_name, m.series_name AS series_name
212 FROM playlist_entry AS e
213 JOIN media AS m ON e.media_id = m.id) AS t
214 GROUP BY t.list_name) AS c
215 ON e.list_name = c.list_name
216 WHERE p.group_name = ? AND
217 p.next_entry + ? <= e.entry AND e.entry < p.next_entry + ?";
218 my @arg = ($group, $o, $n + $o);
219 if (defined $list) { $q .= " AND p.list_name = ?"; push @arg, $list; }
220 $q .= " ORDER BY p.list_name ASC, e.entry ASC";
221 my $st_query = $DB->prepare($q); $st_query->execute(@arg);
222 my $prevlist = undef;
225 while (my @r = $st_query->fetchrow_array) {
226 my ($l, $pos, $i, $nseries, $stitle, $title,
227 $path, $ttn, $loch, $hich) = @r;
230 $out = $nseries == 1 ? $title : "$stitle $title";
232 my $scheme = $path =~ /\.iso$/ ? "dvd" : "file";
234 if ($ttn == -1) { $frag = ""; }
235 elsif ($loch == -1) { $frag = "#$ttn"; }
236 elsif ($hich == -1) { $frag = "#$ttn:$loch"; }
237 else { my $hi = $hich - 1; $frag = "#$ttn:$loch-$ttn:$hi"; }
238 $path = encode_utf8 $path;
239 $path =~ s:([^-_\w.,!\$\%/]):sprintf "%%%02x", ord $1:eg;
240 $out = "$scheme://$ROOT/$path$frag";
244 $out = sprintf "%*s %s",
245 ($n == 1 ? $elen_max : $elen{$l}) + 2, $pos, $out;
247 if ($n > 1) { $out = ($i == $pos ? "> " : " ") . $out; }
248 if (defined $list) { print $out, "\n"; }
249 elsif ($n == 1) { printf "%-*s %s\n", $glen, $l, $out; }
250 elsif (!defined $prevlist) { $prevlist = $l; @out = ($out); }
251 elsif (defined $prevlist && $l eq $prevlist) { push @out, $out; }
253 print "$prevlist\n", map("\t$_\n", @out), "\n";
254 $prevlist = $l; @out = ($out);
257 defined $prevlist and print "$prevlist\n", map "\t$_\n", @out;
260 defcmd "list LIST", sub {
264 getopts("g:r:", \%opt) or $bogusp = 1;
265 @ARGV == 1 or $bogusp = 1;
266 if ($bogusp) { bail_usage; }
271 my $st_nseries = $DB->prepare
272 ("SELECT COUNT(*) FROM
273 (SELECT DISTINCT m.series_name
274 FROM playlist_entry AS e JOIN media AS m ON e.media_id = m.id
275 WHERE e.list_name = ?) AS _");
276 my $st_pos = $DB->prepare
277 ("SELECT p.next_entry FROM playlist_position AS p
278 WHERE p.group_name = ? AND p.list_name = ?");
279 $st_nseries->execute($list);
280 my ($nseries) = $st_nseries->fetchrow_array; $st_nseries->finish;
282 my %pos; my $plen = -1;
283 if (defined $opt{"g"}) {
284 my @g = split /,/, $opt{"g"};
286 my ($pre, $g) = /^ (?: (.*?)=)? (.*) $/x; must_group $g;
287 $st_pos->execute($g, $list); my ($pos) = $st_pos->fetchrow_array;
289 (exists $pos{$pos} ? $pos{$pos} . "," : "") .
290 ($pre // (@g == 1 ? "" : $g));
291 length $t > $plen and $plen = length $t;
295 my $st_elen = $DB->prepare
296 ("SELECT MAX(entry) FROM playlist_entry WHERE list_name = ?");
297 $st_elen->execute($list);
298 my ($emax) = $st_elen->fetchrow_array; $st_elen->finish;
299 my $elen = length $emax;
302 "SELECT e.entry, s.title, m.title
303 FROM playlist_entry AS e
304 JOIN media AS m ON e.media_id = m.id
305 JOIN series AS s ON m.series_name = s.name
306 WHERE e.list_name = ?";
309 if (defined $opt{"r"}) {
310 $opt{"r"} =~ /^ (\d+)? (?: - (\d+)?)? $/x
311 or die "invalid range expression `$opt{'r'}'";
312 my ($lo, $hi) = ($1, $2);
313 if (defined $lo) { $q .= " AND ? <= e.entry"; push @arg, $lo; }
314 if (defined $hi) { $q .= " AND e.entry < ?"; push @arg, $hi; }
317 $q .= " ORDER BY e.entry ASC";
318 my $st_list = $DB->prepare($q); $st_list->execute(@arg);
319 my $msep = $plen < 0 ? "" : " ";
320 while (my @r = $st_list->fetchrow_array) {
321 my ($i, $stitle, $title) = @r;
322 $nseries == 1 or $title = "$stitle $title";
323 my $m = exists $pos{$i} ? "$pos{$i}>" : "";
324 printf "%*s%s%*s %s\n", $plen + 1, $m, $msep, $elen + 2, "[$i]", $title;
330 getopts("h", \%opt) or $bogusp = 1;
331 if ($opt{"h"}) { HELP_MESSAGE \*STDOUT; exit 0; }
332 @ARGV >= 1 or $bogusp = 1;
333 if ($bogusp) { bail_usage; }
335 $CMD = shift; $CMD_FN{$CMD} or die "unknown command `$CMD'";
336 $CMD_FN{$CMD}(@ARGV);
337 if (defined $DB) { $DB->commit; $DB->disconnect; }