Work around missing seriesIndex API by using keyword=index
[python-iview.git] / iview-cli
blob873180321972b9ad710d07f96dec2dfbe73bb02b
1 #!/usr/bin/env python3
3 import sys, os, argparse
4 import os.path
5 import iview.fetch
6 import iview.comm
7 from urllib.error import HTTPError
8 import iview.config
9 import configparser
10 from errno import EPIPE
11 from sys import stderr
12 import urllib.parse
13 from iview.utils import encodeerrors
15 def config():
16     try:
17         iview.comm.get_config()
18     except HTTPError as error:
19         print("""\
20 Error: could not retrieve an important configuration file from iView.
21 Please make sure you are connected to the Internet.
23 If iView works fine in your web browser, the iView API has possibly changed.
24 Try to find an updated version of this program or contact the author.
26 Debug: could not load URL {}""".format(error.url), file=stderr)
27         sys.exit(1)
29 def programme():
30     """Downloads the iView index, and then prints each item in all series.
31     This is a slow function. Using index() and subsequently cherrypicking
32     a series to pass to series() is much faster.
33     """
34     return category('index')
36 def category(keyword):
37     config()
38     index = iview.comm.get_keyword(keyword)
40     if not sys.stdout:
41         return
42     for series in index:
43         sys.stdout.write(encodeerrors(series['title'] + ':\n', sys.stdout))
44         print_series_items(series['items'], indent='\t')
46 def index():
47     """Downloads the iView index, and prints the corresponding series and IDs.
48     """
49     config()
50     index = iview.comm.get_index()
52     if not sys.stdout:
53         return
54     for series in index:
55         title = encodeerrors(series['title'], sys.stdout)
56         sys.stdout.write('{}\t{}\n'.format(series['id'], title))
58 def batch_index():
59     """Downloads the iView index, and prints the corresponding series and IDs in a format
60     that works in the iview batch file
61     """
62     config()
63     index = iview.comm.get_index()
65     if not sys.stdout:
66         return
67     for series in index:
68         title = encodeerrors(series['title'], sys.stdout)
69         sys.stdout.write('{}: {}\n'.format(series['id'], title))
71 def series(series_id):
72     config()
73     print_series_items(iview.comm.get_series_items(series_id))
75 def print_series_items(items, indent=''):
76     if not sys.stdout:
77         return
78     for item in items:
79         title = encodeerrors(item['title'], sys.stdout)
80         sys.stdout.write('{}{}\t({})\n'.format(indent, title, item['url']))
82 def print_auth():
83     config()
84     auth = iview.comm.get_auth()
85     print('iView auth data:')
86     for (desc, key) in (
87         ('Streaming Host', 'host'),
88         ('RTMP Token', 'token'),
89         ('HDS Token', 'tokenhd'),
90         ('Server URL', 'server'),
91         ('Playpath Prefix', 'playpath_prefix'),
92         ('Unmetered', 'free'),
93     ):
94         value = auth.get(key)
95         if value is not None:
96             print('\t{}: {}'.format(desc, value))
98 def download(url, output=None):
99     config()
100     iview.fetch.fetch_program(url, execvp=True, dest_file=output)
102 def batch(batch_file):
103     config()
105     # parse the batch file
106     batch = configparser.ConfigParser()
107     batch.read(os.path.expanduser(batch_file))
108     items = batch.items('batch')
110     batch_destination = '.'
111     series_ids = []
112     last_only = False
114     # separate options from the series ids
115     for key, value in items:
116         if key == 'destination':
117             batch_destination = value
118         elif key == 'last_only':
119             if not(value == '0' or value.lower() == 'false' or value.lower() == "no"):
120                 last_only = True
121         else:
122             # Note: currently the value after the series_id in the batch file
123             # is only used as a comment for the user.
124             series_ids.append(key)
126     # move to where the files should be downloaded
127     os.chdir(batch_destination)
129     # loop through the series ids
130     for series_id in series_ids:
132         # unset the last episode for the current series.
133         last_episode = None
135         # loop through the episodes
136         result = iview.comm.get_series_items(series_id, get_meta=True)
137         [episodes, metadata] = result
138         for episode in episodes:
139             if last_only:
140                 # This last_only feature is experimental, I am not sure which field to 
141                 # use to determine the most recent episode.
142                 if last_episode is None or episode['date'] > last_episode['date']:
143                     last_episode = episode
144             else:
145                 batch_fetch_program(episode, series=metadata['title'])
147         # Last only means we only get one episode for the series
148         if last_only:
149             batch_fetch_program(last_episode, series=metadata['title'])
151 def batch_fetch_program(episode, series):
152     # Only print notification messages for episodes that have never been downloaded before.
153     url = episode['url']
155     # urls sometimes include a path like 'news/' or 'kids/'
156     (pathpart, filepart) = os.path.split(url)
158     # urls also have '.mp4' extension but downloaded files end in '.flv'
159     (base, ext) = os.path.splitext(filepart)
161     title = episode['title']
162     filename = iview.fetch.descriptive_filename(series, title, url)
163     if os.path.isfile(base + '.flv') and not os.path.isfile(filename):
164         print("{} already exists as {}.flv so should be moved".format(filename, base))
165         return
167     if not os.path.isfile(filename):
168         msg = "getting {} - {} -> {}".format(episode['title'], episode['url'], filename)
169         print(msg, file=stderr)
170         iview.fetch.fetch_program(episode['url'], execvp=False, dest_file=filename, quiet=True)
174 def subtitles(name, output=None):
175     config()
177     url = name.rsplit('.', 1)[0]
178     if output is not None:
179         srt = output
180     else:
181         srt = url.rsplit('/', 1)[-1] + '.srt'
183     if not srt == '-' and os.path.isfile(srt):
184         print('Subtitles have already been downloaded to {}'.format(srt), file=stderr)
185         return False
187     msg = 'Downloading subtitles to {}...'.format(srt)
188     print(msg, end=' ', file=stderr)
190     try:
191         subtitles = iview.comm.get_captions(url)
192     except HTTPError as error:
193         print('failed', file=stderr)
194         print('Got an error when downloading {}'.format(error.url), file=stderr)
195         return False
197     if not srt == '-':
198         f = open(srt, 'wb')
199     else:
200         f = sys.stdout.detach()
201         sys.stdout = None
203     with f:
204         f.write(subtitles.encode('utf-8'))
206     print('done', file=stderr)
208 def parse_proxy_argument(proxy):
209     """Try to parse 'proxy' as host:port pair.  Returns an error message
210     if it cannot be understood.  Otherwise, it configures the settings
211     in iview.config and returns None.
212     """
214     try:
215         split = urllib.parse.SplitResult(scheme="", netloc=proxy,
216             path="", query="", fragment="")
217         if split.port is not None:
218             iview.config.socks_proxy_port = split.port
219     except ValueError as err:
220         return err
221     iview.config.socks_proxy_host = split.hostname
223     return None
225 def main():
226     params = argparse.ArgumentParser()
227     params.add_argument("-i", "--index", action="store_true",
228         help="print the iView index (number is the series ID)")
229     params.add_argument("-s", "--series", metavar="<id>",
230         help="get the list of programmes for the series")
231     params.add_argument("-k", "--category", metavar="<keyword>",
232         help="list programmes matching a category keyword")
233     params.add_argument("-p", "--programme", action="store_true",
234         help="""list all iView programmes at once""")
235     params.add_argument("-d", "--download", metavar="<url>",
236         help="""download a programme
237         (pass the url you got from -s, -k or -p)""")
238     params.add_argument("-t", "--subtitles" , metavar="<url>",
239         help="""download subtitles in SRT format for a programme
240         (pass the same url as for --download)""")
241     params.add_argument("-o", "--output", metavar="<file>",
242         help="specify a file to output to (use - for stdout)")
243     params.add_argument("--batch", metavar="<file>",
244         help="specify a batch operation file (for cronjob etc)")
245     params.add_argument("--bindex", action="store_true",
246         help="like --index but output is in batchfile format")
247     params.add_argument("-a", "--print-auth", action="store_true",
248         help="print debug iView auth information")
249     params.add_argument("-c", "--cache", metavar="<dir>",
250         help="use cache directory for debugging")
251     params.add_argument("--host", metavar="<name>",
252         help="override streaming host")
253     params.add_argument("--ip", metavar="<address>",
254         help="send IP address in auth request")
255     params.add_argument("-x", "--proxy", metavar="<host:port>",
256         help="use specified SOCKS proxy")
257     
258     if len(sys.argv) <= 1:
259         params.print_help(stderr)
260         sys.exit(2)
261     args = params.parse_args()
262     
263     if args.proxy is not None:
264         err = parse_proxy_argument(args.proxy)
265         if err is not None:
266             print("Invalid proxy specification: {}\n".format(err), file=stderr)
267             params.print_usage(stderr)
268             sys.exit(4)
269         iview.comm.configure_socks_proxy()
270     if args.cache is not None:
271         iview.config.cache = args.cache
272     if args.host is not None:
273         iview.config.override_host = args.host
274     if args.ip is not None:
275         iview.config.ip = args.ip
277     try:
278         if args.programme:
279             programme()
280         if args.category is not None:
281             category(args.category)
282         if args.index:
283             index()
284         if args.bindex:
285             batch_index()
286         if args.series is not None:
287             series(args.series)
288         if args.print_auth:
289             print_auth()
290         
291         if args.download is not None:
292             download(args.download, args.output)
293         elif args.subtitles is not None:
294             subtitles(args.subtitles, args.output)
295         elif args.batch is not None:
296             batch(args.batch)
297     except iview.comm.Error as error:
298         print(error, file=stderr)
299         sys.exit(1)
301 if __name__ == "__main__":
302     try:
303         main()
304     except KeyboardInterrupt:
305         sys.exit(1)
306     except EnvironmentError as err:
307         if err.errno == EPIPE:
308             sys.exit(1)
309         else:
310             raise