From 4f51c43c9c4499367451bda45cf7c8a3d95dabab Mon Sep 17 00:00:00 2001 From: "Xuan Hu (Sean)" Date: Thu, 2 Jan 2020 17:46:10 +0800 Subject: [PATCH] Initial version of iptv-filter. (#1) --- MANIFEST.in | 3 + Makefile | 23 ++++--- README.md | 62 ++++++------------- config.json | 19 ++++++ defaults.py | 16 ----- helps.py | 29 --------- iptvtools/__init__.py | 0 iptvtools/config.py | 28 +++++++++ iptvtools/constants/__init__.py | 0 iptvtools/constants/defaults.py | 15 +++++ iptvtools/constants/helps.py | 32 ++++++++++ iptvtools/constants/patterns.py | 14 +++++ iptvtools/constants/tags.py | 10 +++ iptvtools/iptv_filter.py | 50 +++++++++++++++ iptvtools/models.py | 98 ++++++++++++++++++++++++++++++ parsers.py => iptvtools/parsers.py | 28 +++++---- iptvtools/utils.py | 76 +++++++++++++++++++++++ main.py | 41 ------------- models.py | 74 ---------------------- patterns.py | 16 ----- setup.py | 65 ++++++++++++++++++++ tags.py | 13 ---- utils.py | 42 ------------- 23 files changed, 460 insertions(+), 294 deletions(-) create mode 100644 MANIFEST.in create mode 100644 config.json delete mode 100644 defaults.py delete mode 100644 helps.py create mode 100644 iptvtools/__init__.py create mode 100644 iptvtools/config.py create mode 100644 iptvtools/constants/__init__.py create mode 100644 iptvtools/constants/defaults.py create mode 100644 iptvtools/constants/helps.py create mode 100644 iptvtools/constants/patterns.py create mode 100644 iptvtools/constants/tags.py create mode 100644 iptvtools/iptv_filter.py create mode 100644 iptvtools/models.py rename parsers.py => iptvtools/parsers.py (56%) mode change 100755 => 100644 create mode 100644 iptvtools/utils.py delete mode 100755 main.py delete mode 100755 models.py delete mode 100755 patterns.py create mode 100644 setup.py delete mode 100755 tags.py delete mode 100755 utils.py diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..443d703 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include LICENSE +include README.md +include config.json diff --git a/Makefile b/Makefile index dbce7d9..7a9cf6c 100644 --- a/Makefile +++ b/Makefile @@ -1,24 +1,31 @@ -.PHONY: clean deps lint pycodestyle pyflakes pylint +.PHONY: clean install dev lint pycodestyle pyflakes pylint dist upload clean: find . -name '*.pyc' -print0 | xargs -0 rm -f find . -name '*.swp' -print0 | xargs -0 rm -f + find . -name '.DS_Store' -print0 | xargs -0 rm -rf find . -name '__pycache__' -print0 | xargs -0 rm -rf -rm -rf build dist *.egg-info .eggs *.spec -deps: - pip install -r requirements.txt - install: - python setup.py install + pip install . + +dev: + pip install .[dev] lint: pycodestyle pyflakes pylint pycodestyle: - -pycodestyle --statistics --count *.py + -pycodestyle setup.py iptvtools pyflakes: - -pyflakes *.py + -pyflakes setup.py iptvtools pylint: - -pylint *.py + -pylint setup.py iptvtools + +dist: + python setup.py sdist bdist_wheel + +upload: + twine upload dist/* diff --git a/README.md b/README.md index 5196bc4..69eea33 100644 --- a/README.md +++ b/README.md @@ -1,56 +1,34 @@ -# iptv-m3u-filter +# iptvtools -Quite a few publicly available IPTV channels can be found on the Internet, but usually only a part of them is valuable considering accessibility, resolution, latency, bandwidth and some other criteria. +[![PyPI version](https://badge.fury.io/py/iptvtools.svg)](https://badge.fury.io/py/iptvtools) -This script aims to help with the filtering purpose. +IPTVTools aims to provide the following features: -## Prerequisites +- [X] Grab well-maintained playlists from the Internet. +- [X] Filter inaccessible channels. +- [X] Filter channels according width/resolution. +- [X] Simple strategy to unify channels' titles. +- [X] Format url with UDP proxy (Udpxy) if provided. +- [X] Fulfill channels' information by referring to well-maintained template. +- [ ] Scan certain ip and port range to find new channels. +- [ ] Establish a lightweight database for routine maintenance. -```shell -make deps -``` +Besides, all scripts should be lightweight and able to keep running regularly after proper configuration. -or +Last but not least, any ideas, comments and suggestions are welcome! + +## Installation ```shell -pip install -r requirements.txt +pip install -U iptvtools ``` -## Usage +or ```shell -$ python main.py -h -usage: main.py [-h] [--min-height MIN_HEIGHT] [-i INPUT] [-o OUTPUT] - [-p MAX_WORKERS] [-t TIMEOUT] - -optional arguments: - -h, --help show this help message and exit - --min-height MIN_HEIGHT - Minimal acceptable height/resolution, defaults to 1080 - which means 1080P. - -i INPUT, --input INPUT - Valid M3U playlists, which could be a file or url, - defaults to `https://iptv- - org.github.io/iptv/index.m3u`. - -o OUTPUT, --output OUTPUT - Output file name, defaults to `iptv-m3u-filter.m3u`. - -p MAX_WORKERS, --max-workers MAX_WORKERS - Number of threads to perform the filtering process, - defaults to 16. - -t TIMEOUT, --timeout TIMEOUT - Acceptable timeout when retrieving stream information, - defaults to 2. +pip install . ``` -## Roadmap - -> Any help is welcome for the following topics. +## Usage -- [ ] Upload to PyPI. -- [ ] Support strategies to unify channel titles. For example, covert `CCTV1`, `CCTV-1HD`, `CCTV-1FHD` to `CCTV-1综合`. -- [ ] Support template combination, such as the [test.m3u](http://epg.51zmt.top:8000/test.m3u) under http://epg.51zmt.top:8000/. -- [ ] Support regular execution, may not by Python code, but usage or documentation instead. -- [ ] Support online deployment, scripts may needed, but still may not by Python code. -- [ ] Support more criteria if possible. -- [ ] Better documentation/docstrings. -- [ ] i18n support. +Please refer to the [wiki](https://github.com/huxuan/iptvtools/wiki) page. diff --git a/config.json b/config.json new file mode 100644 index 0000000..945941e --- /dev/null +++ b/config.json @@ -0,0 +1,19 @@ +{ + "id_filters": { + "-": "", + "IPTV": "", + "BTV冬奥纪实": "北京纪实", + "BTV北京卫视": "北京卫视", + "BTV卡酷少儿": "卡酷动画", + "CETV1": "中国教育1台", + "CETV2": "中国教育2台", + "CETV3": "中国教育3台", + "CETV4": "中国教育4台", + "纪实频道": "上海纪实" + }, + "title_filters": { + "高清": "", + "HD": "", + "+": "+" + } +} diff --git a/defaults.py b/defaults.py deleted file mode 100644 index ab68bf8..0000000 --- a/defaults.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -File: defaults.py -Author: huxuan -Email: i(at)huxuan.org -Description: Defaults value. -""" - - -class Defaults: - INPUT = 'https://iptv-org.github.io/iptv/index.m3u' - MAX_WORKERS = 16 - MIN_HEIGHT = 1080 - OUTPUT = 'iptv-m3u-filter.m3u' - TIMEOUT = 2 diff --git a/helps.py b/helps.py deleted file mode 100644 index 8e51379..0000000 --- a/helps.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -File: helps.py -Author: huxuan -Email: i(at)huxuan.org -Description: Help messages. -""" -from defaults import Defaults - - -class Helps: - INPUT = ( - f'Valid M3U playlists, which could be a file or url, defaults to ' - f'`{Defaults.INPUT}`.' - ) - MAX_WORKERS = ( - f'Number of threads to perform the filtering process, defaults to ' - f'{Defaults.MAX_WORKERS}.' - ) - MIN_HEIGHT = ( - f'Minimal acceptable height/resolution, defaults to ' - f'{Defaults.MIN_HEIGHT} which means {Defaults.MIN_HEIGHT}P.' - ) - OUTPUT = f'Output file name, defaults to `{Defaults.OUTPUT}`.' - TIMEOUT = ( - f'Acceptable timeout when retrieving stream information, defaults to ' - f'{Defaults.TIMEOUT}.' - ) diff --git a/iptvtools/__init__.py b/iptvtools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/iptvtools/config.py b/iptvtools/config.py new file mode 100644 index 0000000..190e619 --- /dev/null +++ b/iptvtools/config.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +File: config.py +Author: huxuan +Email: i(at)huxuan.org +Description: Configuration for iptvtools. +""" +import json +import os +import os.path + + +class Config(): + """Configuration for iptvtools.""" + config = {} + + @classmethod + def init(cls, config_file): + """Initialize configuration.""" + if os.path.isfile(config_file): + with open(config_file) as fin: + cls.config = json.load(fin) + + @classmethod + def __getattr__(cls, key): + """Get configuration with key.""" + return cls.config.get(key, {}) diff --git a/iptvtools/constants/__init__.py b/iptvtools/constants/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/iptvtools/constants/defaults.py b/iptvtools/constants/defaults.py new file mode 100644 index 0000000..c7e9b13 --- /dev/null +++ b/iptvtools/constants/defaults.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +File: constants.py +Author: huxuan +Email: i(at)huxuan.org +Description: Defaults for iptvtools. +""" +CONFIG = 'config.json' +INPUT = 'https://iptv-org.github.io/iptv/index.m3u' +MIN_HEIGHT = 1080 +OUTPUT = 'iptvtools.m3u' +TEMPLATE = None # 'http://epg.51zmt.top:8000/test.m3u' +TIMEOUT = 10 +UDPXY = None diff --git a/iptvtools/constants/helps.py b/iptvtools/constants/helps.py new file mode 100644 index 0000000..6863616 --- /dev/null +++ b/iptvtools/constants/helps.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +File: constants.py +Author: huxuan +Email: i(at)huxuan.org +Description: Helps for iptvtools. +""" +from . import defaults + +CONFIG = ( + f'Configuration file with title and id filter information, defaults to ' + f'`{defaults.CONFIG}`' +) +INPUT = ( + f'Valid M3U playlists, which could be a file or url, defaults to ' + f'`{defaults.INPUT}`.' +) +MIN_HEIGHT = ( + f'Minimal acceptable height/resolution, defaults to ' + f'{defaults.MIN_HEIGHT} which means {defaults.MIN_HEIGHT}P.' +) +OUTPUT = f'Output file name, defaults to `{defaults.OUTPUT}`.' +TEMPLATE = f'Template file name, defaults to `{defaults.TEMPLATE}`.' +TIMEOUT = ( + f'Acceptable timeout when retrieving stream information, defaults to ' + f'{defaults.TIMEOUT}.' +) +UDPXY = ( + f'UDP Proxy which will convert the url automatically, defaults to ' + f'`{defaults.UDPXY}`.' +) diff --git a/iptvtools/constants/patterns.py b/iptvtools/constants/patterns.py new file mode 100644 index 0000000..d76fbb4 --- /dev/null +++ b/iptvtools/constants/patterns.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +File: constants.py +Author: huxuan +Email: i(at)huxuan.org +Description: Patterns for iptvtools. +""" +import re + +PARAMS = re.compile(r'(\S+)="(.*?)"') +EXTINF = re.compile( + r'^#EXTINF:(?P-?\d+?) ?(?P.*),(?P.*?)$') +EXTM3U = re.compile(r'^#EXTM3U ?(?P<params>.*)$') diff --git a/iptvtools/constants/tags.py b/iptvtools/constants/tags.py new file mode 100644 index 0000000..a9ce973 --- /dev/null +++ b/iptvtools/constants/tags.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +File: constants.py +Author: huxuan +Email: i(at)huxuan.org +Description: Tags for iptvtools. +""" +M3U = '#EXTM3U' +INF = '#EXTINF' diff --git a/iptvtools/iptv_filter.py b/iptvtools/iptv_filter.py new file mode 100644 index 0000000..50feda0 --- /dev/null +++ b/iptvtools/iptv_filter.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +File: main.py +Author: huxuan +Email: i(at)huxuan.org +Description: Filter IPTV m3u playlists according to customized criteria. +""" +import argparse + +from .config import Config +from .constants import defaults +from .constants import helps +from .models import Playlist + + +def parse_args(): + """Arguments Parsers.""" + parser = argparse.ArgumentParser() + parser.add_argument('--min-height', default=defaults.MIN_HEIGHT, type=int, + help=helps.MIN_HEIGHT) + parser.add_argument('-c', '--config', default=defaults.CONFIG, + help=helps.CONFIG) + parser.add_argument('-i', '--input', default=defaults.INPUT, + help=helps.INPUT) + parser.add_argument('-o', '--output', default=defaults.OUTPUT, + help=helps.OUTPUT) + parser.add_argument('-t', '--template', default=defaults.TEMPLATE, + help=helps.TEMPLATE) + parser.add_argument('-T', '--timeout', default=defaults.TIMEOUT, type=int, + help=helps.TIMEOUT) + parser.add_argument('-u', '--udpxy', default=defaults.UDPXY, + help=helps.UDPXY) + return parser.parse_args() + + +def main(): + """Main process.""" + args = parse_args() + Config.init(args.config) + playlist = Playlist() + playlist.parse(args.input, args.udpxy) + playlist.parse(args.template, is_template=True) + playlist.filter(args.min_height) + print(f'{len(playlist.data)} channels after filtering!') + open(args.output, 'w', encoding='utf-8').write(str(playlist)) + + +if __name__ == '__main__': + main() diff --git a/iptvtools/models.py b/iptvtools/models.py new file mode 100644 index 0000000..7a8832c --- /dev/null +++ b/iptvtools/models.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +File: models.py +Author: huxuan +Email: i(at)huxuan.org +Description: Playlist which contains all the channels' information. +""" +import sys + +from tqdm import tqdm + +from . import parsers +from . import utils +from .constants import tags + + +class Playlist(): + """Playlist model.""" + def __init__(self): + self.data = {} + self.template = {} + self.tvg_url = None + + def __str__(self): + res = [] + res.append(tags.M3U) + if self.tvg_url is not None: + res[0] += f' x-tvg-url="{self.tvg_url}"' + urls = sorted(self.data, key=self.__sort_by_tvg_id_and_title) + for url in urls: + internal_id = self.data[url]['id'] + if internal_id in self.template.keys(): + entry = self.template[internal_id] + else: + entry = self.data[url] + params_dict = entry.get('params', {}) + params = ' '.join([f'{key}="{value}"' + for key, value in params_dict.items()]) + res.append( + f'{tags.INF}:{entry["duration"]} {params},{entry["title"]}' + ) + res.append(url) + return '\n'.join(res) + + def parse(self, content, udpxy=None, is_template=False): + """Parse content.""" + lines = parsers.parse_content_to_lines(content) + + if lines[0].startswith(tags.M3U): + res = parsers.parse_tag_m3u(lines[0]) + if res.get('tvg-url'): + self.tvg_url = res.get('tvg-url') + lines = lines[1:] + + current_item = {} + for line in lines: + line = line.strip() + if not line: + continue + if line.startswith(tags.INF): + current_item = parsers.parse_tag_inf(line) + current_item = utils.filter_title_and_id(current_item) + else: + if is_template: + self.template[current_item['id']] = current_item + else: + if udpxy: + line = utils.convert_url_with_udpxy(line, udpxy) + self.data[line] = current_item + + def filter(self, min_height=0): + """Filter process.""" + flag_stream = min_height > 0 + if flag_stream: + urls = list(self.data.keys()) + # for url in urls: # Mostly used for debug. + for url in tqdm(urls, ascii=True): + stream_info = utils.probe(url) + if stream_info: + if utils.max_height(stream_info) < min_height: + self.data.pop(url) + else: + self.data.pop(url) + + def __sort_by_tvg_id_and_title(self, url): + """Sort by tvg-id and title.""" + internal_id = self.data[url]['id'] + tvg_id = sys.maxsize + title = self.data[url]['title'] + if internal_id in self.template.keys(): + tvg_id = int(self.template[internal_id]['params']['tvg-id']) + title = self.template[internal_id]['title'] + return tvg_id, title + + def __sort_by_data_title(self, url): + """Sort by title in data.""" + return self.data[url]['title'] diff --git a/parsers.py b/iptvtools/parsers.py old mode 100755 new mode 100644 similarity index 56% rename from parsers.py rename to iptvtools/parsers.py index f048bf1..f76a358 --- a/parsers.py +++ b/iptvtools/parsers.py @@ -9,38 +9,40 @@ from urllib.request import urlopen import os.path -from patterns import Patterns +from .constants import patterns def parse_content_to_lines(content): + """Universal interface to split content into lines.""" if os.path.isfile(content): return _parse_from_file(content) return _parse_from_url(content) def parse_tag_inf(line): - match = Patterns.EXTINF.fullmatch(line) - return _transform_match_result(match) + """Parse INF content.""" + match = patterns.EXTINF.fullmatch(line) + res = match.groupdict() + if 'params' in res: + res['params'] = dict(patterns.PARAMS.findall(res['params'])) + return res def parse_tag_m3u(line): - match = Patterns.EXTM3U.fullmatch(line) - return _transform_match_result(match) + """Parse M3U content.""" + match = patterns.EXTM3U.fullmatch(line) + return match.groupdict() def _parse_from_file(filename): + """Parse content from file.""" + print(f'Retrieving playlists from file: {filename}') with open(filename, encoding='utf-8') as fin: return fin.read().splitlines() def _parse_from_url(url): - print(f'Retrieving playlists from {url}') + """Parse content from url.""" + print(f'Retrieving playlists from url: {url}') with urlopen(url) as response: return response.read().decode('utf-8').splitlines() - - -def _transform_match_result(match): - res = match.groupdict() - if res.get('params'): - res['params'] = dict(Patterns.PARAMS.findall(res['params'])) - return res diff --git a/iptvtools/utils.py b/iptvtools/utils.py new file mode 100644 index 0000000..61ec2db --- /dev/null +++ b/iptvtools/utils.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +File: utils.py +Author: huxuan +Email: i(at)huxuan.org +Description: Relevant Utilities. +""" +from subprocess import PIPE +from subprocess import Popen +from subprocess import TimeoutExpired +from urllib.parse import urlparse +import json + +from .config import Config + +PROBE_COMMAND = ( + 'ffprobe -hide_banner -show_streams -select_streams v ' + '-of json=c=1 -v quiet' +) + + +def convert_url_with_udpxy(orig_url, udpxy): + """Convert url with udpxy.""" + orig_url = orig_url.replace('///', '//') # Hack to some abnormal urls. + parsed_url = urlparse(orig_url) + new_url = f'{udpxy}/{parsed_url.scheme}/{parsed_url.netloc}' + return new_url + + +def filter_title_and_id(item): + """Filter title and id.""" + for title_filter in sorted(Config().title_filters): + if title_filter in item['title']: + item['title'] = item['title'].replace( + title_filter, + Config().title_filters[title_filter]) + + if 'tvg-name' in item.get('params'): + item['id'] = item['params']['tvg-name'] + else: + item['id'] = item['title'] + + for id_filter in sorted(Config().id_filters): + if id_filter in item['id']: + item['id'] = item['id'].replace( + id_filter, + Config().id_filters[id_filter]) + + return item + + +def max_height(stream_info): + """Get max height from stream information.""" + if stream_info.get('streams'): + return max([ + stream.get('height', 0) + for stream in stream_info['streams'] + ]) + return 0 + + +def probe(url, timeout=None): + """Invoke probe to get stream information.""" + outs = None + proc = Popen(f'{PROBE_COMMAND} {url}'.split(), stdout=PIPE, stderr=PIPE) + try: + outs, dummy = proc.communicate(timeout=timeout) + except TimeoutExpired: + proc.kill() + if outs: + try: + return json.loads(outs.decode('utf-8')) + except json.JSONDecodeError as exc: + print(exc) + return None diff --git a/main.py b/main.py deleted file mode 100755 index 3a2dccf..0000000 --- a/main.py +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -File: main.py -Author: huxuan -Email: i(at)huxuan.org -Description: Main entrance for iptv-m3u-filer. -""" -import argparse - -from defaults import Defaults -from helps import Helps -from models import M3U8 - - -def parse_args(): - parser = argparse.ArgumentParser() - parser.add_argument('--min-height', default=Defaults.MIN_HEIGHT, type=int, - help=Helps.MIN_HEIGHT) - parser.add_argument('-i', '--input', default=Defaults.INPUT, - help=Helps.INPUT) - parser.add_argument('-o', '--output', default=Defaults.OUTPUT, - help=Helps.OUTPUT) - parser.add_argument('-p', '--max-workers', default=Defaults.MAX_WORKERS, - type=int, help=Helps.MAX_WORKERS) - parser.add_argument('-t', '--timeout', default=2, type=int, - help=Helps.TIMEOUT) - return parser.parse_args() - - -def main(): - args = parse_args() - m3u8 = M3U8() - m3u8.parse(args.input) - m3u8.filter(args) - print(f'{len(m3u8.data)} channels after filtering!') - open(args.output, 'w', encoding='utf-8').write(str(m3u8)) - - -if __name__ == '__main__': - main() diff --git a/models.py b/models.py deleted file mode 100755 index b82c13d..0000000 --- a/models.py +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -File: models.py -Author: huxuan -Email: i(at)huxuan.org -Description: Simplified M3U8 and other related models. -""" -from concurrent import futures -from concurrent.futures import ThreadPoolExecutor - -from tqdm import tqdm - -from tags import Tags -import parsers -import utils - - -class M3U8(): - def __init__(self): - self.data = {} - self.tvg_url = None - - def __str__(self): - res = [] - res.append(Tags.M3U) - if self.tvg_url is not None: - res[0] += f' x-tvg-url="{self.tvg_url}"' - for url, entry in self.data.items(): - params_dict = entry.get('params', {}) - params = ' '.join([f'{key}="{value}"' - for key, value in params_dict.items()]) - res.append( - f'{Tags.INF}:{entry["duration"]} {params},{entry["title"]}' - ) - res.append(url) - return '\n'.join(res) - - def parse(self, content): - lines = parsers.parse_content_to_lines(content) - - if lines[0].startswith(Tags.M3U): - res = parsers.parse_tag_m3u(lines[0]) - if res.get('tvg-url'): - self.tvg_url = res.get('tvg-url') - lines = lines[1:] - - current_item = {} - for line in lines: - if line.startswith(Tags.INF): - current_item = parsers.parse_tag_inf(line) - else: - self.data[line] = current_item - - def filter(self, args): - flag_stream = args.min_height > 0 - - with ThreadPoolExecutor(max_workers=args.max_workers) as executor: - if flag_stream: - - future_dict = { - executor.submit(utils.probe, url, args.timeout): url - for url in self.data} - - for future in tqdm(futures.as_completed(future_dict), - total=len(self.data), - ascii=True): - stream_info = future.result() - url = future_dict[future] - if stream_info: - if utils.max_height(stream_info) < args.min_height: - self.data.pop(url) - else: - self.data.pop(url) diff --git a/patterns.py b/patterns.py deleted file mode 100755 index e7d86f3..0000000 --- a/patterns.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -File: patterns.py -Author: huxuan -Email: i(at)huxuan.org -Description: Relevant regular expression patterns. -""" -import re - - -class Patterns: - PARAMS = re.compile(r'(\S+)="(.*?)"') - EXTINF = re.compile( - r'^#EXTINF:(?P<duration>-?\d+?) ?(?P<params>.*),(?P<title>.*?)$') - EXTM3U = re.compile(r'^#EXTM3U ?(?P<params>.*)$') diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..b614dbc --- /dev/null +++ b/setup.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +File: setup.py +Author: huxuan +Email: i(at)huxuan.org +Description: Python packaging for iptvtools. +""" +from setuptools import find_packages +from setuptools import setup + +DESCRIPTION = ( + 'A set of scripts that help to better IPTV experience.' +) + +CLASSIFIERS = [ + 'Development Status :: 4 - Beta', + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3 :: Only', + 'Topic :: Utilities' +] + +INSTALL_REQUIRES = [ + 'tqdm' +] + +DEV_REQUIRES = [ + 'pycodestyle', + 'pyflakes', + 'pylint' +] + +EXTRAS_REQUIRE = { + 'dev': DEV_REQUIRES +} + + +def readme(): + """Parse README for long_description.""" + with open('README.md') as fin: + return fin.read() + + +setup(name='iptvtools', + version='0.1', + description=DESCRIPTION, + long_description=readme(), + long_description_content_type='text/markdown', + classifiers=CLASSIFIERS, + keywords='iptv m3u playlist tools filter', + url='https://github.com/huxuan/iptvtools', + author='Xuan (Sean) Hu', + author_email='i@huxuan.org', + license='MIT', + packages=find_packages(), + install_requires=INSTALL_REQUIRES, + extras_require=EXTRAS_REQUIRE, + python_requires='>=3', + entry_points={ + 'console_scripts': [ + 'iptv-filter=iptvtools.iptv_filter:main' + ], + }, + include_package_data=True) diff --git a/tags.py b/tags.py deleted file mode 100755 index 206a7f7..0000000 --- a/tags.py +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -File: tags.py -Author: huxuan -Email: i(at)huxuan.org -Description: Relevant m3u8 tags. -""" - - -class Tags: - M3U = '#EXTM3U' - INF = '#EXTINF' diff --git a/utils.py b/utils.py deleted file mode 100755 index 1391e52..0000000 --- a/utils.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -File: utils.py -Author: huxuan -Email: i(at)huxuan.org -Description: Relevant Utilities. -""" -from json import JSONDecodeError -from subprocess import PIPE -from subprocess import Popen -from subprocess import TimeoutExpired -import json - -PROBE_COMMAND = ( - 'ffprobe -hide_banner -show_streams -select_streams v ' - '-of json=c=1 -v quiet' -) - - -def probe(url, timeout=None): - outs = None - proc = Popen(f'{PROBE_COMMAND} {url}'.split(), stdout=PIPE, stderr=PIPE) - try: - outs, dummy = proc.communicate(timeout=timeout) - except TimeoutExpired: - proc.kill() - if outs: - try: - return json.loads(outs.decode('utf-8')) - except JSONDecodeError as exc: - print(exc) - return None - - -def max_height(stream_info): - if stream_info.get('streams'): - return max([ - stream.get('height', 0) - for stream in stream_info['streams'] - ]) - return 0