From 49337f4dbb9b0f10b7d20845b356b8f6142ad440 Mon Sep 17 00:00:00 2001 From: thevickypedia Date: Tue, 16 Jan 2024 12:21:01 +0530 Subject: [PATCH] Auto convert `.srt` to .vtt files for subtitles Add shutdown tasks to delete files created during runtime Update README.md --- README.md | 3 +- doc_gen/index.rst | 7 +++++ docs/README.html | 5 +-- docs/README.md | 3 +- docs/_sources/README.md.txt | 3 +- docs/_sources/index.rst.txt | 7 +++++ docs/genindex.html | 30 +++++++++++++++--- docs/index.html | 34 +++++++++++++++++--- docs/objects.inv | Bin 911 -> 942 bytes docs/py-modindex.html | 5 +++ docs/searchindex.js | 2 +- pystream/main.py | 28 ++++++++++++++--- pystream/models/config.py | 11 +++++-- pystream/models/images.py | 6 +--- pystream/models/subtitles.py | 23 ++++++++++++++ pystream/routers/video.py | 24 ++++++++------ pystream/templates/index.html | 57 +++++++++++++++++----------------- 17 files changed, 183 insertions(+), 65 deletions(-) create mode 100644 pystream/models/subtitles.py diff --git a/README.md b/README.md index efb02a7..511a105 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ python -m pip install stream-localhost ## Sample Usage ```python +import asyncio import os import pystream @@ -28,7 +29,7 @@ if __name__ == '__main__': ) # Add the following to host on local IP address, skip for localhost (127.0.0.1) # kwargs["video_host"] = pystream.utils.get_local_ip() - pystream.start(**kwargs) + asyncio.run(pystream.start(**kwargs)) ``` ### Env Variables diff --git a/doc_gen/index.rst b/doc_gen/index.rst index 9e405b6..ff8317e 100644 --- a/doc_gen/index.rst +++ b/doc_gen/index.rst @@ -56,6 +56,13 @@ Stream :members: :undoc-members: +Subtitles +========= + +.. automodule:: pystream.models.subtitles + :members: + :undoc-members: + Routers ======= Authentication diff --git a/docs/README.html b/docs/README.html index 6c94745..e94c8c0 100644 --- a/docs/README.html +++ b/docs/README.html @@ -59,7 +59,8 @@

Install

Sample Usage

-
import os
+
 
diff --git a/docs/README.md b/docs/README.md index efb02a7..511a105 100644 --- a/docs/README.md +++ b/docs/README.md @@ -17,6 +17,7 @@ python -m pip install stream-localhost ## Sample Usage ```python +import asyncio import os import pystream @@ -28,7 +29,7 @@ if __name__ == '__main__': ) # Add the following to host on local IP address, skip for localhost (127.0.0.1) # kwargs["video_host"] = pystream.utils.get_local_ip() - pystream.start(**kwargs) + asyncio.run(pystream.start(**kwargs)) ``` ### Env Variables diff --git a/docs/_sources/README.md.txt b/docs/_sources/README.md.txt index efb02a7..511a105 100644 --- a/docs/_sources/README.md.txt +++ b/docs/_sources/README.md.txt @@ -17,6 +17,7 @@ python -m pip install stream-localhost ## Sample Usage ```python +import asyncio import os import pystream @@ -28,7 +29,7 @@ if __name__ == '__main__': ) # Add the following to host on local IP address, skip for localhost (127.0.0.1) # kwargs["video_host"] = pystream.utils.get_local_ip() - pystream.start(**kwargs) + asyncio.run(pystream.start(**kwargs)) ``` ### Env Variables diff --git a/docs/_sources/index.rst.txt b/docs/_sources/index.rst.txt index 9e405b6..ff8317e 100644 --- a/docs/_sources/index.rst.txt +++ b/docs/_sources/index.rst.txt @@ -56,6 +56,13 @@ Stream :members: :undoc-members: +Subtitles +========= + +.. automodule:: pystream.models.subtitles + :members: + :undoc-members: + Routers ======= Authentication diff --git a/docs/genindex.html b/docs/genindex.html index d4d02da..11df95a 100644 --- a/docs/genindex.html +++ b/docs/genindex.html @@ -43,6 +43,7 @@

Index

A | C + | D | E | F | G @@ -77,6 +78,14 @@

C

+

D

+ + +
+

E

- + + + +
-
  • pystream.models.images @@ -272,6 +281,8 @@

    P

  • module
+
    pystream.models.stream
    + pystream.models.subtitles +
    diff --git a/docs/searchindex.js b/docs/searchindex.js index b5021eb..cbba7a6 100644 --- a/docs/searchindex.js +++ b/docs/searchindex.js @@ -1 +1 @@ -Search.setIndex({"docnames": ["README", "index"], "filenames": ["README.md", "index.rst"], "titles": ["Video Streaming", "Stream-Localhost - A secured interface to stream videos"], "terms": {"deploy": 0, "python": 0, "modul": 0, "via": 0, "authent": 0, "session": [0, 1], "us": [0, 1], "fastapi": [0, 1], "m": 0, "pip": 0, "localhost": 0, "import": 0, "o": 0, "pystream": [0, 1], "__name__": 0, "__main__": 0, "kwarg": [0, 1], "dict": [0, 1], "usernam": [0, 1], "foo": 0, "password": [0, 1], "bar": 0, "video_sourc": [0, 1], "path": [0, 1], "join": 0, "expandus": 0, "download": 0, "add": [0, 1], "follow": 0, "host": [0, 1], "local": [0, 1], "ip": [0, 1], "address": [0, 1], "skip": 0, "127": [0, 1], "0": [0, 1], "1": [0, 1], "video_host": [0, 1], "util": 0, "get_local_ip": [0, 1], "start": [0, 1], "bulb": 0, "environ": [0, 1], "can": 0, "load": [0, 1], "from": [0, 1], "ani": 0, "file": [0, 1], "filenam": [0, 1], "default": 0, "To": 0, "custom": 0, "set": 0, "var": [0, 1], "env_fil": [0, 1], "kei": [0, 1], "its": [0, 1], "valu": [0, 1], "mandatori": 0, "choic": 0, "_": 0, "sourc": [0, 1], "underscor": 0, "ignor": [0, 1], "option": [0, 1], "port": 0, "number": [0, 1], "applic": 0, "8000": [0, 1], "format": 0, "sequenc": [0, 1], "support": 0, "mp4": [0, 1], "mov": [0, 1], "worker": [0, 1], "spin": 0, "up": 0, "uvicorn": 0, "server": 0, "websit": [0, 1], "list": [0, 1], "regex": 0, "cor": 0, "configur": [0, 1], "requir": [0, 1], "onli": [0, 1], "tunnel": 0, "cdn": 0, "auto": 0, "thumbnail": [0, 1], "boolean": [0, 1], "flag": [0, 1], "gener": [0, 1], "imag": 0, "preview": [0, 1], "true": [0, 1], "docstr": 0, "googl": 0, "style": 0, "convent": 0, "pep": 0, "8": 0, "isort": 0, "gitvers": 0, "revers": 0, "f": 0, "release_not": 0, "rst": 0, "t": 0, "pre": 0, "commit": 0, "ensur": 0, "run": [0, 1], "pytest": 0, "valid": [0, 1], "hyperlink": 0, "all": [0, 1], "markdown": 0, "includ": [0, 1], "wiki": 0, "page": [0, 1], "sphinx": 0, "5": 0, "recommonmark": 0, "http": [0, 1], "org": 0, "project": 0, "thevickypedia": 0, "github": 0, "io": 0, "vignesh": 0, "rao": 0, "under": 0, "mit": 0, "instal": 1, "sampl": 1, "usag": 1, "code": 1, "standard": 1, "releas": 1, "note": 1, "lint": 1, "pypi": 1, "packag": 1, "runbook": 1, "licens": 1, "copyright": 1, "startup_task": 1, "none": 1, "task": 1, "need": 1, "dure": 1, "api": 1, "startup": 1, "starter": 1, "function": 1, "paramet": 1, "keyword": 1, "argument": 1, "env": 1, "async": 1, "verifi": 1, "credenti": 1, "httpbasiccredenti": 1, "jsonrespons": 1, "client": 1, "return": 1, "json": 1, "respons": 1, "content": 1, "statu": 1, "type": 1, "class": 1, "envconfig": 1, "_case_sensit": 1, "bool": 1, "_env_prefix": 1, "str": 1, "_env_fil": 1, "dotenvtyp": 1, "posixpath": 1, "_env_file_encod": 1, "_env_nested_delimit": 1, "_secrets_dir": 1, "secretstr": 1, "ipv4address": 1, "video_port": 1, "int": 1, "file_format": 1, "auto_thumbnail": 1, "pydant": 1, "share": 1, "across": 1, "creat": 1, "new": 1, "pars": 1, "input": 1, "data": 1, "rais": 1, "validationerror": 1, "pydantic_cor": 1, "cannot": 1, "form": 1, "__init__": 1, "__pydantic_self__": 1, "instead": 1, "more": 1, "common": 1, "self": 1, "first": 1, "arg": 1, "allow": 1, "field": 1, "name": 1, "variabl": 1, "env_prefix": 1, "extra": 1, "hide_input_in_error": 1, "classmethod": 1, "parse_video_host": 1, "string": 1, "notion": 1, "object": 1, "parse_websit": 1, "evalu": 1, "fileio": 1, "index": 1, "html": 1, "list_fil": 1, "static": 1, "track": 1, "query_param": 1, "index_endpoint": 1, "logout_endpoint": 1, "logout": 1, "streaming_endpoint": 1, "chunk_siz": 1, "1048576": 1, "store": 1, "info": 1, "inform": 1, "alia": 1, "filepath": 1, "initi": 1, "captur": 1, "frame": 1, "particular": 1, "instanti": 1, "opencv": 1, "": 1, "videocaptur": 1, "generate_thumbnail": 1, "interv": 1, "output_dir": 1, "second": 1, "output": 1, "directori": 1, "success": 1, "failur": 1, "get_video_length": 1, "tupl": 1, "timedelta": 1, "get": 1, "calcul": 1, "length": 1, "datetim": 1, "generate_preview": 1, "at_second": 1, "time": 1, "which": 1, "should": 1, "log_connect": 1, "request": 1, "log": 1, "connect": 1, "devic": 1, "thi": 1, "avoid": 1, "multipl": 1, "when": 1, "same": 1, "differ": 1, "natural_sort_kei": 1, "union": 1, "sort": 1, "natur": 1, "wai": 1, "take": 1, "an": 1, "element": 1, "deriv": 1, "split": 1, "part": 1, "regular": 1, "express": 1, "get_dir_stream_cont": 1, "parent": 1, "subdir": 1, "insid": 1, "displai": 1, "login": 1, "subdirectori": 1, "within": 1, "exist": 1, "dictionari": 1, "pair": 1, "get_all_stream_cont": 1, "folder": 1, "contain": 1, "each": 1, "section": 1, "get_it": 1, "purepath": 1, "current": 1, "serv": 1, "previou": 1, "next": 1, "render": 1, "remove_thumbnail": 1, "img_path": 1, "trigger": 1, "timer": 1, "remov": 1, "send_bytes_range_request": 1, "file_obj": 1, "binaryio": 1, "start_rang": 1, "end_rang": 1, "asynciter": 1, "bytestr": 1, "send": 1, "chunk": 1, "rang": 1, "specif": 1, "rfc7233": 1, "byte": 1, "end": 1, "yield": 1, "iter": 1, "get_range_head": 1, "range_head": 1, "file_s": 1, "proce": 1, "header": 1, "size": 1, "range_requests_respons": 1, "file_path": 1, "streamingrespons": 1, "given": 1, "auth": 1, "depend": 1, "httpbasic": 1, "templaterespons": 1, "handler": 1, "templat": 1, "redirectrespons": 1, "401": 1, "out": 1, "user": 1, "httpexcept": 1, "messag": 1, "get_favicon": 1, "filerespons": 1, "favicon": 1, "ico": 1, "endpoint": 1, "robinhood": 1, "script": 1, "root": 1, "redirect": 1, "preview_load": 1, "ha": 1, "track_load": 1, "track_path": 1, "subtitl": 1, "stream_video": 1, "video_path": 1, "video_endpoint": 1, "receiv": 1, "cooki": 1, "rootfilt": 1, "filter": 1, "while": 1, "preserv": 1, "other": 1, "access": 1, "200": 1, "ok": 1, "307": 1, "temporari": 1, "vid_nam": 1, "redund": 1, "pass": 1, "overrid": 1, "implement": 1, "subclass": 1, "The": 1, "method": 1, "record": 1, "examin": 1, "fals": 1, "discard": 1, "togeth": 1, "children": 1, "have": 1, "event": 1, "through": 1, "If": 1, "i": 1, "specifi": 1, "everi": 1, "logrecord": 1, "repres": 1, "someth": 1, "simpl": 1, "check": 1, "network": 1, "id": 1, "retriev": 1, "privat": 1, "machin": 1, "get_public_ip": 1, "extract": 1, "public": 1, "make": 1, "extern": 1, "search": 1}, "objects": {"pystream": [[1, 0, 0, "-", "logger"], [1, 0, 0, "-", "main"], [1, 0, 0, "-", "utils"]], "pystream.logger": [[1, 1, 1, "", "RootFilter"]], "pystream.logger.RootFilter": [[1, 2, 1, "", "filter"]], "pystream.main": [[1, 3, 1, "", "start"], [1, 3, 1, "", "startup_tasks"]], "pystream.models": [[1, 0, 0, "-", "authenticator"], [1, 0, 0, "-", "config"], [1, 0, 0, "-", "images"], [1, 0, 0, "-", "squire"], [1, 0, 0, "-", "stream"]], "pystream.models.authenticator": [[1, 3, 1, "", "verify"]], "pystream.models.config": [[1, 1, 1, "", "EnvConfig"], [1, 1, 1, "", "FileIO"], [1, 1, 1, "", "Session"], [1, 1, 1, "", "Static"], [1, 4, 1, "", "env"]], "pystream.models.config.EnvConfig": [[1, 1, 1, "", "Config"], [1, 4, 1, "", "auto_thumbnail"], [1, 4, 1, "", "file_formats"], [1, 2, 1, "", "parse_video_host"], [1, 2, 1, "", "parse_website"], [1, 4, 1, "", "password"], [1, 4, 1, "", "username"], [1, 4, 1, "", "video_host"], [1, 4, 1, "", "video_port"], [1, 4, 1, "", "video_source"], [1, 4, 1, "", "website"], [1, 4, 1, "", "workers"]], "pystream.models.config.EnvConfig.Config": [[1, 4, 1, "", "env_file"], [1, 4, 1, "", "env_prefix"], [1, 4, 1, "", "extra"], [1, 4, 1, "", "hide_input_in_errors"]], "pystream.models.config.FileIO": [[1, 4, 1, "", "index"], [1, 4, 1, "", "list_files"]], "pystream.models.config.Session": [[1, 4, 1, "", "info"]], "pystream.models.config.Static": [[1, 4, 1, "", "chunk_size"], [1, 4, 1, "", "index_endpoint"], [1, 4, 1, "", "logout_endpoint"], [1, 4, 1, "", "preview"], [1, 4, 1, "", "query_param"], [1, 4, 1, "", "stream"], [1, 4, 1, "", "streaming_endpoint"], [1, 4, 1, "", "track"]], "pystream.models.images": [[1, 1, 1, "", "Images"]], "pystream.models.images.Images": [[1, 2, 1, "", "generate_preview"], [1, 2, 1, "", "generate_thumbnails"], [1, 2, 1, "", "get_video_length"]], "pystream.models.squire": [[1, 3, 1, "", "get_all_stream_content"], [1, 3, 1, "", "get_dir_stream_content"], [1, 3, 1, "", "get_iter"], [1, 3, 1, "", "log_connection"], [1, 3, 1, "", "natural_sort_key"], [1, 3, 1, "", "remove_thumbnail"]], "pystream.models.stream": [[1, 3, 1, "", "get_range_header"], [1, 3, 1, "", "range_requests_response"], [1, 3, 1, "", "send_bytes_range_requests"]], "pystream.routers": [[1, 0, 0, "-", "auth"], [1, 0, 0, "-", "basics"], [1, 0, 0, "-", "video"]], "pystream.routers.auth": [[1, 3, 1, "", "index"], [1, 3, 1, "", "logout"]], "pystream.routers.basics": [[1, 3, 1, "", "get_favicon"], [1, 3, 1, "", "root"]], "pystream.routers.video": [[1, 3, 1, "", "preview_loader"], [1, 3, 1, "", "stream_video"], [1, 3, 1, "", "track_loader"], [1, 3, 1, "", "video_endpoint"]], "pystream.utils": [[1, 3, 1, "", "get_local_ip"], [1, 3, 1, "", "get_public_ip"]]}, "objtypes": {"0": "py:module", "1": "py:class", "2": "py:method", "3": "py:function", "4": "py:attribute"}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "class", "Python class"], "2": ["py", "method", "Python method"], "3": ["py", "function", "Python function"], "4": ["py", "attribute", "Python attribute"]}, "titleterms": {"video": [0, 1], "stream": [0, 1], "instal": 0, "sampl": 0, "usag": 0, "env": 0, "variabl": 0, "code": 0, "standard": 0, "releas": 0, "note": 0, "lint": 0, "pypi": 0, "packag": 0, "runbook": 0, "licens": 0, "copyright": 0, "localhost": 1, "A": 1, "secur": 1, "interfac": 1, "read": 1, "me": 1, "main": 1, "modul": 1, "model": 1, "authent": 1, "config": 1, "imag": 1, "squir": 1, "router": 1, "basic": 1, "support": 1, "logger": 1, "util": 1, "indic": 1, "tabl": 1}, "envversion": {"sphinx.domains.c": 2, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 6, "sphinx.domains.index": 1, "sphinx.domains.javascript": 2, "sphinx.domains.math": 2, "sphinx.domains.python": 3, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx": 56}}) \ No newline at end of file +Search.setIndex({"docnames": ["README", "index"], "filenames": ["README.md", "index.rst"], "titles": ["Video Streaming", "Stream-Localhost - A secured interface to stream videos"], "terms": {"deploy": 0, "python": 0, "modul": 0, "via": 0, "authent": 0, "session": [0, 1], "us": [0, 1], "fastapi": [0, 1], "m": 0, "pip": 0, "localhost": 0, "import": 0, "asyncio": 0, "o": 0, "pystream": [0, 1], "__name__": 0, "__main__": 0, "kwarg": [0, 1], "dict": [0, 1], "usernam": [0, 1], "foo": 0, "password": [0, 1], "bar": 0, "video_sourc": [0, 1], "path": [0, 1], "join": 0, "expandus": 0, "download": 0, "add": [0, 1], "follow": 0, "host": [0, 1], "local": [0, 1], "ip": [0, 1], "address": [0, 1], "skip": 0, "127": [0, 1], "0": [0, 1], "1": [0, 1], "video_host": [0, 1], "util": 0, "get_local_ip": [0, 1], "run": [0, 1], "start": [0, 1], "bulb": 0, "environ": [0, 1], "can": 0, "load": [0, 1], "from": [0, 1], "ani": 0, "file": [0, 1], "filenam": [0, 1], "default": 0, "To": 0, "custom": 0, "set": [0, 1], "var": [0, 1], "env_fil": [0, 1], "kei": [0, 1], "its": [0, 1], "valu": [0, 1], "mandatori": 0, "choic": 0, "_": 0, "sourc": [0, 1], "underscor": 0, "ignor": [0, 1], "option": [0, 1], "port": 0, "number": [0, 1], "applic": 0, "8000": [0, 1], "format": 0, "sequenc": [0, 1], "support": 0, "mp4": [0, 1], "mov": [0, 1], "worker": [0, 1], "spin": 0, "up": 0, "uvicorn": 0, "server": 0, "websit": [0, 1], "list": [0, 1], "regex": 0, "cor": 0, "configur": [0, 1], "requir": [0, 1], "onli": [0, 1], "tunnel": 0, "cdn": 0, "auto": 0, "thumbnail": [0, 1], "boolean": [0, 1], "flag": [0, 1], "gener": [0, 1], "imag": 0, "preview": [0, 1], "true": [0, 1], "docstr": 0, "googl": 0, "style": 0, "convent": 0, "pep": 0, "8": 0, "isort": 0, "gitvers": 0, "revers": 0, "f": 0, "release_not": 0, "rst": 0, "t": 0, "pre": 0, "commit": 0, "ensur": 0, "pytest": 0, "valid": [0, 1], "hyperlink": 0, "all": [0, 1], "markdown": 0, "includ": [0, 1], "wiki": 0, "page": [0, 1], "sphinx": 0, "5": 0, "recommonmark": 0, "http": [0, 1], "org": 0, "project": 0, "thevickypedia": 0, "github": 0, "io": 0, "vignesh": 0, "rao": 0, "under": 0, "mit": 0, "instal": 1, "sampl": 1, "usag": 1, "code": 1, "standard": 1, "releas": 1, "note": 1, "lint": 1, "pypi": 1, "packag": 1, "runbook": 1, "licens": 1, "copyright": 1, "async": 1, "startup_task": 1, "none": 1, "task": 1, "need": 1, "dure": 1, "api": 1, "startup": 1, "shutdown_task": 1, "shutdown": 1, "starter": 1, "function": 1, "paramet": 1, "keyword": 1, "argument": 1, "env": 1, "verifi": 1, "credenti": 1, "httpbasiccredenti": 1, "jsonrespons": 1, "client": 1, "return": 1, "json": 1, "respons": 1, "content": 1, "statu": 1, "type": 1, "class": 1, "envconfig": 1, "_case_sensit": 1, "bool": 1, "_env_prefix": 1, "str": 1, "_env_fil": 1, "dotenvtyp": 1, "posixpath": 1, "_env_file_encod": 1, "_env_nested_delimit": 1, "_secrets_dir": 1, "secretstr": 1, "ipv4address": 1, "video_port": 1, "int": 1, "file_format": 1, "auto_thumbnail": 1, "pydant": 1, "share": 1, "across": 1, "creat": 1, "new": 1, "pars": 1, "input": 1, "data": 1, "rais": 1, "validationerror": 1, "pydantic_cor": 1, "cannot": 1, "form": 1, "__init__": 1, "__pydantic_self__": 1, "instead": 1, "more": 1, "common": 1, "self": 1, "first": 1, "arg": 1, "allow": 1, "field": 1, "name": 1, "variabl": 1, "env_prefix": 1, "extra": 1, "hide_input_in_error": 1, "classmethod": 1, "parse_video_host": 1, "string": 1, "notion": 1, "object": 1, "parse_websit": 1, "evalu": 1, "fileio": 1, "index": 1, "html": 1, "list_fil": 1, "static": 1, "track": 1, "query_param": 1, "index_endpoint": 1, "logout_endpoint": 1, "logout": 1, "streaming_endpoint": 1, "chunk_siz": 1, "1048576": 1, "delet": 1, "store": 1, "info": 1, "inform": 1, "alia": 1, "filepath": 1, "initi": 1, "captur": 1, "frame": 1, "particular": 1, "instanti": 1, "opencv": 1, "": 1, "videocaptur": 1, "generate_thumbnail": 1, "interv": 1, "output_dir": 1, "second": 1, "output": 1, "directori": 1, "success": 1, "failur": 1, "get_video_length": 1, "tupl": 1, "timedelta": 1, "get": 1, "calcul": 1, "length": 1, "datetim": 1, "generate_preview": 1, "at_second": 1, "time": 1, "which": 1, "should": 1, "log_connect": 1, "request": 1, "log": 1, "connect": 1, "devic": 1, "thi": 1, "avoid": 1, "multipl": 1, "when": 1, "same": 1, "differ": 1, "natural_sort_kei": 1, "union": 1, "sort": 1, "natur": 1, "wai": 1, "take": 1, "an": 1, "element": 1, "deriv": 1, "split": 1, "part": 1, "regular": 1, "express": 1, "get_dir_stream_cont": 1, "parent": 1, "subdir": 1, "insid": 1, "displai": 1, "login": 1, "subdirectori": 1, "within": 1, "exist": 1, "dictionari": 1, "pair": 1, "get_all_stream_cont": 1, "folder": 1, "contain": 1, "each": 1, "section": 1, "get_it": 1, "purepath": 1, "current": 1, "serv": 1, "previou": 1, "next": 1, "render": 1, "remove_thumbnail": 1, "img_path": 1, "trigger": 1, "timer": 1, "remov": 1, "send_bytes_range_request": 1, "file_obj": 1, "binaryio": 1, "start_rang": 1, "end_rang": 1, "asynciter": 1, "bytestr": 1, "send": 1, "chunk": 1, "rang": 1, "specif": 1, "rfc7233": 1, "byte": 1, "end": 1, "yield": 1, "iter": 1, "get_range_head": 1, "range_head": 1, "file_s": 1, "proce": 1, "header": 1, "size": 1, "range_requests_respons": 1, "file_path": 1, "streamingrespons": 1, "given": 1, "srt_to_vtt": 1, "convert": 1, "srt": 1, "vtt": 1, "compat": 1, "j": 1, "auth": 1, "depend": 1, "httpbasic": 1, "templaterespons": 1, "handler": 1, "templat": 1, "redirectrespons": 1, "401": 1, "out": 1, "user": 1, "httpexcept": 1, "messag": 1, "get_favicon": 1, "filerespons": 1, "favicon": 1, "ico": 1, "endpoint": 1, "robinhood": 1, "script": 1, "root": 1, "redirect": 1, "preview_load": 1, "ha": 1, "track_load": 1, "track_path": 1, "stream_video": 1, "video_path": 1, "video_endpoint": 1, "receiv": 1, "cooki": 1, "rootfilt": 1, "filter": 1, "while": 1, "preserv": 1, "other": 1, "access": 1, "200": 1, "ok": 1, "307": 1, "temporari": 1, "vid_nam": 1, "redund": 1, "pass": 1, "overrid": 1, "implement": 1, "subclass": 1, "The": 1, "method": 1, "record": 1, "examin": 1, "fals": 1, "discard": 1, "togeth": 1, "children": 1, "have": 1, "event": 1, "through": 1, "If": 1, "i": 1, "specifi": 1, "everi": 1, "logrecord": 1, "repres": 1, "someth": 1, "simpl": 1, "check": 1, "network": 1, "id": 1, "retriev": 1, "privat": 1, "machin": 1, "get_public_ip": 1, "extract": 1, "public": 1, "make": 1, "extern": 1, "search": 1}, "objects": {"pystream": [[1, 0, 0, "-", "logger"], [1, 0, 0, "-", "main"], [1, 0, 0, "-", "utils"]], "pystream.logger": [[1, 1, 1, "", "RootFilter"]], "pystream.logger.RootFilter": [[1, 2, 1, "", "filter"]], "pystream.main": [[1, 3, 1, "", "shutdown_tasks"], [1, 3, 1, "", "start"], [1, 3, 1, "", "startup_tasks"]], "pystream.models": [[1, 0, 0, "-", "authenticator"], [1, 0, 0, "-", "config"], [1, 0, 0, "-", "images"], [1, 0, 0, "-", "squire"], [1, 0, 0, "-", "stream"], [1, 0, 0, "-", "subtitles"]], "pystream.models.authenticator": [[1, 3, 1, "", "verify"]], "pystream.models.config": [[1, 1, 1, "", "EnvConfig"], [1, 1, 1, "", "FileIO"], [1, 1, 1, "", "Session"], [1, 1, 1, "", "Static"], [1, 4, 1, "", "env"]], "pystream.models.config.EnvConfig": [[1, 1, 1, "", "Config"], [1, 4, 1, "", "auto_thumbnail"], [1, 4, 1, "", "file_formats"], [1, 2, 1, "", "parse_video_host"], [1, 2, 1, "", "parse_website"], [1, 4, 1, "", "password"], [1, 4, 1, "", "username"], [1, 4, 1, "", "video_host"], [1, 4, 1, "", "video_port"], [1, 4, 1, "", "video_source"], [1, 4, 1, "", "website"], [1, 4, 1, "", "workers"]], "pystream.models.config.EnvConfig.Config": [[1, 4, 1, "", "env_file"], [1, 4, 1, "", "env_prefix"], [1, 4, 1, "", "extra"], [1, 4, 1, "", "hide_input_in_errors"]], "pystream.models.config.FileIO": [[1, 4, 1, "", "index"], [1, 4, 1, "", "list_files"]], "pystream.models.config.Session": [[1, 4, 1, "", "info"]], "pystream.models.config.Static": [[1, 4, 1, "", "chunk_size"], [1, 4, 1, "", "deletions"], [1, 4, 1, "", "index_endpoint"], [1, 4, 1, "", "logout_endpoint"], [1, 4, 1, "", "preview"], [1, 4, 1, "", "query_param"], [1, 4, 1, "", "stream"], [1, 4, 1, "", "streaming_endpoint"], [1, 4, 1, "", "track"]], "pystream.models.images": [[1, 1, 1, "", "Images"]], "pystream.models.images.Images": [[1, 2, 1, "", "generate_preview"], [1, 2, 1, "", "generate_thumbnails"], [1, 2, 1, "", "get_video_length"]], "pystream.models.squire": [[1, 3, 1, "", "get_all_stream_content"], [1, 3, 1, "", "get_dir_stream_content"], [1, 3, 1, "", "get_iter"], [1, 3, 1, "", "log_connection"], [1, 3, 1, "", "natural_sort_key"], [1, 3, 1, "", "remove_thumbnail"]], "pystream.models.stream": [[1, 3, 1, "", "get_range_header"], [1, 3, 1, "", "range_requests_response"], [1, 3, 1, "", "send_bytes_range_requests"]], "pystream.models.subtitles": [[1, 3, 1, "", "srt_to_vtt"]], "pystream.routers": [[1, 0, 0, "-", "auth"], [1, 0, 0, "-", "basics"], [1, 0, 0, "-", "video"]], "pystream.routers.auth": [[1, 3, 1, "", "index"], [1, 3, 1, "", "logout"]], "pystream.routers.basics": [[1, 3, 1, "", "get_favicon"], [1, 3, 1, "", "root"]], "pystream.routers.video": [[1, 3, 1, "", "preview_loader"], [1, 3, 1, "", "stream_video"], [1, 3, 1, "", "track_loader"], [1, 3, 1, "", "video_endpoint"]], "pystream.utils": [[1, 3, 1, "", "get_local_ip"], [1, 3, 1, "", "get_public_ip"]]}, "objtypes": {"0": "py:module", "1": "py:class", "2": "py:method", "3": "py:function", "4": "py:attribute"}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "class", "Python class"], "2": ["py", "method", "Python method"], "3": ["py", "function", "Python function"], "4": ["py", "attribute", "Python attribute"]}, "titleterms": {"video": [0, 1], "stream": [0, 1], "instal": 0, "sampl": 0, "usag": 0, "env": 0, "variabl": 0, "code": 0, "standard": 0, "releas": 0, "note": 0, "lint": 0, "pypi": 0, "packag": 0, "runbook": 0, "licens": 0, "copyright": 0, "localhost": 1, "A": 1, "secur": 1, "interfac": 1, "read": 1, "me": 1, "main": 1, "modul": 1, "model": 1, "authent": 1, "config": 1, "imag": 1, "squir": 1, "subtitl": 1, "router": 1, "basic": 1, "support": 1, "logger": 1, "util": 1, "indic": 1, "tabl": 1}, "envversion": {"sphinx.domains.c": 2, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 6, "sphinx.domains.index": 1, "sphinx.domains.javascript": 2, "sphinx.domains.math": 2, "sphinx.domains.python": 3, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx": 56}}) \ No newline at end of file diff --git a/pystream/main.py b/pystream/main.py index 18013fb..a726fb4 100644 --- a/pystream/main.py +++ b/pystream/main.py @@ -1,3 +1,5 @@ +import os + import uvicorn from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware @@ -12,8 +14,7 @@ app.include_router(video.router) -# noinspection HttpUrlsUsage -def startup_tasks() -> None: +async def startup_tasks() -> None: """Tasks that need to run during the API startup.""" logger.info('Setting CORS policy.') origins = ["http://localhost.com", "https://localhost.com"] @@ -22,7 +23,19 @@ def startup_tasks() -> None: app.add_middleware(CORSMiddleware, allow_origins=origins) -def start(**kwargs) -> None: +async def shutdown_tasks() -> None: + """Tasks that need to run during the API shutdown.""" + logger.info('Deleting %d files created during runtime.', len(config.static.deletions)) + logger.debug(config.static.deletions) + for file in config.static.deletions: + if file.exists(): + logger.debug("Deleting file: %s", file) + os.remove(file) + else: + logger.warning("File '%s' does not exist", file) + + +async def start(**kwargs) -> None: """Starter function for the streaming API. Args: @@ -47,7 +60,12 @@ def start(**kwargs) -> None: "log_config": log_config, "workers": config.env.workers } + uvicorn_config = uvicorn.Config(**argument_dict) + uvicorn_server = uvicorn.Server(config=uvicorn_config) # Run startup tasks - startup_tasks() - uvicorn.run(**argument_dict) + logger.info("Initiating startup tasks") + await startup_tasks() + await uvicorn_server.serve() # Await uvicorn server + logger.info("Initiating shutdown tasks") + await shutdown_tasks() diff --git a/pystream/models/config.py b/pystream/models/config.py index 569d96d..d73c47f 100644 --- a/pystream/models/config.py +++ b/pystream/models/config.py @@ -1,7 +1,8 @@ import os +import pathlib import socket from ipaddress import IPv4Address -from typing import List, Sequence, Union +from typing import List, Sequence, Set, Union from pydantic import (BaseModel, DirectoryPath, Field, PositiveInt, SecretStr, field_validator) @@ -48,8 +49,11 @@ def parse_website(cls, value: str) -> List[str]: if not value: return [] val = eval(value) - assert isinstance(val, list) - return val + if isinstance(val, list): + return val + raise ValueError( + f"Invalid value for website, must be a list of strings, got {type(val)}" + ) class FileIO(BaseModel): @@ -78,6 +82,7 @@ class Static(BaseModel): logout_endpoint: str = "/logout" streaming_endpoint: str = "/video" chunk_size: PositiveInt = 1024 * 1024 + deletions: Set[pathlib.PosixPath] = set() class Session(BaseModel): diff --git a/pystream/models/images.py b/pystream/models/images.py index ff02b75..354cd01 100644 --- a/pystream/models/images.py +++ b/pystream/models/images.py @@ -76,7 +76,7 @@ def generate_preview(self, path: str, at_second: int = None) -> bool: """Generate preview image for a video. Args: - path: Path to store the preview image. + path: Filepath to store the preview image. at_second: Time in seconds at which the image should be captured for preview. Returns: @@ -84,10 +84,6 @@ def generate_preview(self, path: str, at_second: int = None) -> bool: Returns a boolean flag to indicate success/failure. """ seconds, video_time = self.get_video_length() - if os.path.isdir(path): - raise IsADirectoryError("Requires a filepath, received a directory path.") - if os.path.isfile(path): - logger.warning("%s will be overwritten", path) if at_second: assert at_second <= seconds, f"Frame at {at_second}s is beyond the video duration of {seconds}s" else: diff --git a/pystream/models/subtitles.py b/pystream/models/subtitles.py new file mode 100644 index 0000000..ec06aeb --- /dev/null +++ b/pystream/models/subtitles.py @@ -0,0 +1,23 @@ +import pathlib + + +async def srt_to_vtt(filename: pathlib.PosixPath) -> None: + """Convert a .srt file to .vtt for subtitles to be compatible with video-js. + + Args: + filename: Name of the srt file. + """ + output_file = filename.with_suffix('.vtt') + with open(filename, 'r', encoding='utf-8') as rf: + srt_content = rf.read() + srt_content = srt_content.replace(',', '.') + srt_content = srt_content.replace(' --> ', '-->') + vtt_content = 'WEBVTT\n\n' + subtitle_blocks = srt_content.strip().split('\n\n') + for block in subtitle_blocks: + lines = block.split('\n') + timecode = lines[1] + text = '\n'.join(lines[2:]) + vtt_content += f"{timecode}\n{text}\n\n" + with open(output_file, 'w', encoding='utf-8') as wf: + wf.write(vtt_content) diff --git a/pystream/routers/video.py b/pystream/routers/video.py index d75e325..c3d5ac1 100644 --- a/pystream/routers/video.py +++ b/pystream/routers/video.py @@ -1,7 +1,6 @@ import html import os import pathlib -import threading from typing import Optional, Union from urllib import parse as urlparse @@ -10,8 +9,8 @@ from fastapi.security import HTTPBasicCredentials from pystream.logger import logger -from pystream.models import authenticator, config, squire, stream -from pystream.models.images import Images +from pystream.models import (authenticator, config, images, squire, stream, + subtitles) from pystream.routers import auth router = APIRouter() @@ -35,8 +34,7 @@ async def preview_loader(request: Request, img_path: str, squire.log_connection(request) img_path = pathlib.PosixPath(html.unescape(img_path)) if img_path.exists(): - # Image is required only for caching in the browser, so it is not required after it has been rendered - threading.Timer(interval=5, function=squire.remove_thumbnail, args=(img_path,)).start() + config.static.deletions.add(img_path) return FileResponse(img_path) logger.critical("'%s' not found", img_path) raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"{img_path!r} NOT FOUND") @@ -105,12 +103,20 @@ async def stream_video(request: Request, # Uses preview file if exists at source, else tries to create one at video_source (reuses when refreshed) pys_preview = os.path.join(pure_path.parent, f"_{pure_path.name.replace(pure_path.suffix, '_pys_preview.jpg')}") - if os.path.isfile(pys_preview) or Images(filepath=pure_path).generate_preview(pys_preview): + if os.path.isfile(pys_preview) or images.Images(filepath=pure_path).generate_preview(pys_preview): preview_src = pys_preview attrs['preview'] = urlparse.quote(f"/{config.static.preview}/{preview_src}") - sub = os.path.join(pure_path.parent, pure_path.name.replace(pure_path.suffix, '.vtt')) - if os.path.isfile(sub): - attrs['track'] = urlparse.quote(f"/{config.static.track}/{sub}") + sfx = pathlib.PosixPath(str(os.path.join(pure_path.parent, pure_path.name.replace(pure_path.suffix, '')))) + vtt = sfx.with_suffix('.vtt') + srt = sfx.with_suffix('.srt') + if vtt.exists(): + attrs['track'] = urlparse.quote(f"/{config.static.track}/{vtt}") + elif srt.exists(): + logger.info("Converting '%s.srt' to '%s.vtt' for subtitles", sfx.name, sfx.name) + await subtitles.srt_to_vtt(srt) + if vtt.exists(): + config.static.deletions.add(vtt) + attrs['track'] = urlparse.quote(f"/{config.static.track}/{vtt}") return auth.templates.TemplateResponse(name=config.fileio.index, headers=None, context=attrs) raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Video file {video_path!r} not found") diff --git a/pystream/templates/index.html b/pystream/templates/index.html index 1ef149f..2049656 100644 --- a/pystream/templates/index.html +++ b/pystream/templates/index.html @@ -84,7 +84,7 @@