summaryrefslogtreecommitdiffstats
path: root/TOOLS/umpv
blob: 69aeb8a54a18a6a685aaff5335c91a7d064ae8a4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
#!/usr/bin/env python

"""
This script emulates "unique application" functionality on Linux. When starting
playback with this script, it will try to reuse an already running instance of
mpv (but only if that was started with umpv). Other mpv instances (not started
by umpv) are ignored, and the script doesn't know about them.

This only takes filenames as arguments. Custom options can't be used; the script
interprets them as filenames. If mpv is already running, the files passed to
umpv are appended to mpv's internal playlist. If a file does not exist or is
otherwise not playable, mpv will skip the playlist entry when attempting to
play it (from the GUI perspective, it's silently ignored).

If mpv isn't running yet, this script will start mpv and let it control the
current terminal. It will not write output to stdout/stderr, because this
will typically just fill ~/.xsession-errors with garbage.

mpv will terminate if there are no more files to play, and running the umpv
script after that will start a new mpv instance.

Note that you can control the mpv instance by writing to the command fifo:

    echo "cycle fullscreen" > /tmp/umpv-fifo-*

Note: you can supply custom options with the UMPV_OPTIONS environment variable.
      The environment variable will be split on whitespace, and each item is
      passed as option to mpv _if_ the script starts mpv. If mpv is not started
      by the script (i.e. mpv is already running), the options will be ignored.

Warning:

The script attempts to make sure the FIFO is safely created (i.e. not world-
writable), and checks that it's really a FIFO. This is important for security,
because the FIFO allows anyone with write access to run arbitrary commands
in mpv's context using the "run" input command. If you are worried about
security, you should verify that the code handles these concerns correctly.
"""

import sys
import os
import errno
import subprocess
import fcntl
import stat
import string

if len(sys.argv) < 1:
    sys.exit(1)
files = sys.argv[1:]

# this is about the same method mpv uses to decide this
def is_url(filename):
    parts = filename.split(":", 1)
    if len(parts) < 2:
        return False
    # protocol prefix has no special characters => it's an URL
    prefix = parts[0]
    for c in prefix:
        if string.ascii_letters.find(c) < 0:
            return False
    return True

# make them absolute; also makes them safe against interpretation as options
def make_abs(filename):
    if not is_url(filename):
        return os.path.abspath(filename)
    return filename
files = [make_abs(f) for f in files]

opts_env = os.getenv("UMPV_OPTIONS")
opts_env = opts_env.split() if opts_env else []

FIFO = "/tmp/umpv-fifo-" + os.getenv("USER")

fifo_fd = -1
try:
    fifo_fd = os.open(FIFO, os.O_NONBLOCK | os.O_WRONLY)
except OSError as e:
    if e.errno == errno.ENXIO:
        pass  # pipe has no writer
    elif e.errno == errno.ENOENT:
        pass # doesn't exist
    else:
        raise e

if fifo_fd >= 0:
    st = os.fstat(fifo_fd)
    if (((st.st_mode & (stat.S_IRWXG | stat.S_IRWXO)) != 0) or
        (not stat.S_ISFIFO(st.st_mode)) or
        (st.st_uid != os.getuid())):
        print("error: command FIFO is not a FIFO or has bogus permissions")
        sys.exit(1)

if fifo_fd >= 0:
    # Unhandled race condition: what if mpv is terminating right now?
    fcntl.fcntl(fifo_fd, fcntl.F_SETFL, 0) # set blocking mode
    fifo = os.fdopen(fifo_fd, "w")
    for f in files:
        # escape: \ \n "
        f = f.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n")
        f = "\"" + f + "\""
        fifo.write("raw loadfile " + f + " append\n")
else:
    # Recreate pipe if it doesn't already exist.
    # Also makes sure it's safe, and no other user can create a bogus pipe
    # that breaks security.
    try:
        os.unlink(FIFO)
    except OSError as e:
        if e.errno == errno.ENOENT:
            pass
        else:
            raise e
    os.mkfifo(FIFO, int("0600", 8))

    opts = ["mpv", "--no-terminal", "--force-window", "--input-file=" + FIFO]
    opts.extend(opts_env)
    opts.append("--")
    opts.extend(files)

    subprocess.check_call(opts)