├── kodi_strm ├── __main__.py ├── cli.py ├── file_handler.py └── drive_handler.py ├── strm-generator.py ├── .gitignore ├── setup.bat ├── .github ├── workflows │ ├── black.yml │ └── build.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── .stale.yml └── dependabot.yml ├── setup.sh ├── requirements.txt ├── LICENSE └── readme.md /kodi_strm/__main__.py: -------------------------------------------------------------------------------- 1 | from . import cli 2 | 3 | if __name__ == "__main__": 4 | cli.main() 5 | -------------------------------------------------------------------------------- /strm-generator.py: -------------------------------------------------------------------------------- 1 | import kodi_strm.cli as cli 2 | 3 | if __name__ == "__main__": 4 | cli.main() 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The virtual environment. 2 | .venv/ 3 | venv/ 4 | 5 | # Cache files 6 | .vscode/ 7 | __pycache__ 8 | 9 | # A bash script that I'm using to port the script on different systems 10 | # setup.** -- Adding this file too, might ease the setup for some people. 11 | 12 | # Configuration files to interact with Google Drive API. 13 | **/**.pickle 14 | **/**.json 15 | 16 | # Cache directory generated by PyCharm. 17 | .idea/ 18 | 19 | # Ignoring anything present under the test directory 20 | test_** 21 | -------------------------------------------------------------------------------- /setup.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | echo Upgrading pip 4 | python -m pip install --upgrade pip 5 | 6 | echo Installing virtual-environment 7 | pip install --upgrade virtualenv 8 | 9 | echo Creating a virutal environment 10 | virtualenv venv 11 | 12 | REM Activating the virtual environment. 13 | venv\Scripts\activate 14 | 15 | echo Installing the required packages 16 | pip install -r requirements.txt 17 | 18 | echo Setup executed successfully. 19 | 20 | echo Activate the virtual-environment using 21 | echo `venv\Scripts\activate` 22 | echo 23 | echo Once done, follow the rest of instructions in the setup. 24 | -------------------------------------------------------------------------------- /.github/workflows/black.yml: -------------------------------------------------------------------------------- 1 | name: Black 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches-ignore: 7 | - "draft/**" 8 | - "docs/**" 9 | pull_request: 10 | types: [ opened, reopened ] 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | python-version: [ "3.9" ] 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v4 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | 27 | - uses: psf/black@stable -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | echo -e "\nUpgrading pip" 2 | python -m pip install --upgrade pip 3 | 4 | echo -e "\nInstall virtual-environment" 5 | pip install --upgrade virtualenv 6 | 7 | echo -e "\nCreating a virutal environment" 8 | virtualenv ./venv 9 | 10 | # Activating the virtual environment. 11 | source venv/bin/activate 12 | 13 | echo -e "\nInstalling the required packages" 14 | pip install -r ./requirements.txt 15 | 16 | echo -e " 17 | Setup executed successfully. 18 | 19 | Activate the virtual-environment using 20 | \`source venv/bin/activate\` 21 | 22 | Once done, follow the rest of instructions in the setup. 23 | " 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | backports.shutil-get-terminal-size==1.0.0 2 | cachetools==5.3.0 3 | certifi==2022.12.7 4 | charset-normalizer==2.1.1 5 | click==8.1.3 6 | colorama==0.4.6 7 | google-api-core==2.11.0 8 | google-api-python-client==2.80.0 9 | google-auth==2.16.2 10 | google-auth-httplib2==0.1.0 11 | google-auth-oauthlib==1.0.0 12 | googleapis-common-protos==1.58.0 13 | httplib2==0.21.0 14 | idna==3.4 15 | oauthlib==3.2.2 16 | packaging==23.0 17 | protobuf==4.22.0 18 | pyasn1==0.4.8 19 | pyasn1-modules==0.2.8 20 | pyparsing==3.0.9 21 | reprint==0.6.0 22 | requests==2.28.2 23 | requests-oauthlib==1.3.1 24 | rsa==4.9 25 | six==1.16.0 26 | typer==0.7.0 27 | upgrade-requirements==1.7.0 28 | uritemplate==4.1.1 29 | urllib3==1.26.14 30 | -------------------------------------------------------------------------------- /.github/.stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | 4 | # Number of days of inactivity before a stale issue is closed 5 | daysUntilClose: 7 6 | 7 | # Issues with these labels will never be considered stale 8 | exemptLabels: 9 | - pinned 10 | - security 11 | - open 12 | 13 | # Label to use when marking an issue as stale 14 | staleLabel: stale 15 | 16 | # Comment to post when marking an issue as stale. Set to `false` to disable 17 | markComment: > 18 | This issue has been automatically marked as stale because it does not have any 19 | recent activity. It will be closed if no further activity occurs. Thank you 20 | for your contributions. 21 | 22 | # Comment to post when closing a stale issue. Set to `false` to disable 23 | closeComment: true 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Demon Rem 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. Assume that you're explaining the problem to a kid. Explain in detail. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behaviour: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behaviour** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Please fill the following information:** 27 | - OS: [e.g. Windows/Linux] 28 | - Browser [e.g. chrome, safari] 29 | 30 | **Packages Installed:** 31 | Run `pip freeze > packages.txt` this will create a file called `packages.txt` in the working directory. Copy and paste the contents of the file in this section. 32 | Note: If you are using a virtual environment, remember to activate it *before* firing the pip freeze command. 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Builder 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches-ignore: 7 | - "draft/**" 8 | - "docs/**" 9 | pull_request: 10 | types: [ opened, reopened ] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | python-version: [ "3.9" ] 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v4 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | 27 | - name: Set up cache 28 | id: setup-cache 29 | uses: actions/cache@v3 30 | with: 31 | path: .venv 32 | key: .venv-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }} 33 | 34 | - name: Install Dependencies 35 | if: steps.setup-cache.outputs.cache-hit != 'true' 36 | run: | 37 | pip install --upgrade virtualenv 38 | virtualenv .venv 39 | source .venv/bin/activate 40 | python -m pip install --upgrade pip 41 | pip install -r requirements.txt 42 | 43 | - name: Test 44 | run: | 45 | source .venv/bin/activate 46 | python -m kodi_strm --help 47 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 8 | reviewers: 9 | - notsatan 10 | ignore: 11 | - dependency-name: astroid 12 | versions: 13 | - 2.5.2 14 | - 2.5.3 15 | - dependency-name: pylint 16 | versions: 17 | - 2.7.4 18 | - dependency-name: google-api-python-client 19 | versions: 20 | - 2.1.0 21 | - dependency-name: virtualenv 22 | versions: 23 | - 20.4.0 24 | - 20.4.1 25 | - 20.4.3 26 | - dependency-name: google-api-core 27 | versions: 28 | - 1.25.0 29 | - 1.25.1 30 | - 1.26.0 31 | - 1.26.2 32 | - dependency-name: google-auth-oauthlib 33 | versions: 34 | - 0.4.3 35 | - dependency-name: google-auth-httplib2 36 | versions: 37 | - 0.1.0 38 | - dependency-name: google-auth 39 | versions: 40 | - 1.25.0 41 | - 1.26.1 42 | - 1.27.0 43 | - 1.28.0 44 | - dependency-name: autopep8 45 | versions: 46 | - 1.5.6 47 | - dependency-name: googleapis-common-protos 48 | versions: 49 | - 1.53.0 50 | - dependency-name: isort 51 | versions: 52 | - 5.7.0 53 | - dependency-name: urllib3 54 | versions: 55 | - 1.26.3 56 | - dependency-name: rsa 57 | versions: 58 | - "4.7" 59 | - 4.7.1 60 | - dependency-name: pytz 61 | versions: 62 | - "2020.5" 63 | -------------------------------------------------------------------------------- /kodi_strm/cli.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | import shutil 4 | import sysconfig 5 | from os.path import exists as path_exists 6 | from os.path import join as join_path 7 | from pathlib import Path 8 | from typing import Optional 9 | 10 | import typer 11 | from reprint import output 12 | 13 | from kodi_strm.drive_handler import DriveHandler 14 | from kodi_strm.file_handler import FileHandler 15 | 16 | __VERSION: Optional[str] = "2.0.0" 17 | __APP_NAME: Optional[str] = "kodi-strm" 18 | 19 | # Switch to flip all flags to be case in/sensitive 20 | __CASE_SENSITIVE: bool = False 21 | 22 | 23 | def __callback_version(fired: bool): 24 | """ 25 | Callback function - fired when the `--version` flag is invoked. Check the value 26 | of `fired` to know if the user has actually used the flag 27 | """ 28 | 29 | if not fired: 30 | return # flag was not invoked 31 | 32 | typer.echo( 33 | f"{__APP_NAME} v{__VERSION}\n" 34 | + f"- os/kernel: {platform.release()}\n" 35 | + f"- os/type: {sysconfig.get_platform()}\n" 36 | + f"- os/machine: {platform.machine()}\n" 37 | + f"- os/arch: {platform.architecture()[0]}\n" 38 | + f"- python/version: {platform.python_version()}\n", 39 | ) 40 | 41 | raise typer.Exit() # direct exit 42 | 43 | 44 | def __check_collisions(dst: str, force: bool): 45 | """ 46 | Checks against collisions for the destination path 47 | """ 48 | 49 | if not path_exists(dst): 50 | return # direct return 51 | 52 | if force: 53 | # Force flag enabled, direct remove and exit 54 | shutil.rmtree(dst) 55 | return 56 | 57 | typer.secho( 58 | f"Destination directory `{dst}` already exists\n" 59 | + "Proceed by wiping the existing directory?", 60 | err=True, 61 | fg=typer.colors.RED, 62 | ) 63 | 64 | while True: 65 | typer.echo("y/n> ", nl=False) 66 | choice = str(input()) 67 | 68 | if choice.lower() == "n": 69 | raise typer.Abort() 70 | elif choice.lower() == "y": 71 | shutil.rmtree(dst) 72 | typer.echo(f"Successfully wiped path '{dst}'\n", err=True) 73 | return 74 | else: 75 | typer.secho(f"\tUnexpected input: `{choice}`\n", fg=typer.colors.RED) 76 | 77 | 78 | def cmd_interface( 79 | source: Optional[str] = typer.Option( 80 | None, 81 | "--source", 82 | show_default=False, 83 | case_sensitive=__CASE_SENSITIVE, 84 | help="Folder ID for source directory on Google Drive", 85 | ), 86 | destination: Optional[Path] = typer.Option( 87 | None, 88 | "--dest", 89 | "--destination", 90 | exists=True, # path needs to exist 91 | writable=True, # ensures a writeable path 92 | dir_okay=True, # allows path to directory 93 | file_okay=False, # rejects path to a file 94 | resolve_path=True, # resolves complete path 95 | case_sensitive=__CASE_SENSITIVE, 96 | help="Destination directory where `strm` files will be placed", 97 | ), 98 | root_name: Optional[str] = typer.Option( 99 | None, 100 | "--root", 101 | "--rootname", 102 | case_sensitive=__CASE_SENSITIVE, 103 | help="Custom name for the source directory", 104 | ), 105 | rem_extensions: bool = typer.Option( 106 | False, 107 | "--no-ext", 108 | "--no-extensions", 109 | show_default=False, 110 | case_sensitive=__CASE_SENSITIVE, 111 | help="Remove original extensions from generated strm files", 112 | ), 113 | hide_updates: bool = typer.Option( 114 | False, 115 | "--no-updates", 116 | show_default=False, 117 | case_sensitive=__CASE_SENSITIVE, 118 | help="Disable live progress/updates", 119 | ), 120 | force: bool = typer.Option( 121 | False, 122 | "--force", 123 | "-f", 124 | show_default=True, 125 | case_sensitive=__CASE_SENSITIVE, 126 | help="Wipe out root directory (if exists) in case of a collision", 127 | ), 128 | version: bool = typer.Option( 129 | None, 130 | "--version", 131 | "-v", 132 | is_eager=True, 133 | callback=__callback_version, 134 | case_sensitive=__CASE_SENSITIVE, 135 | help="Display current app version", 136 | ), 137 | ) -> None: 138 | drive_handler = DriveHandler() # authenticate drive api 139 | 140 | with output(output_type="list", initial_len=9, interval=500) as outstream: 141 | # Replace destination directory with the current directory path if not supplied 142 | destination = destination if destination else os.getcwd() 143 | file_handler = FileHandler( 144 | destination=destination, 145 | include_extensions=not rem_extensions, 146 | live_updates=not hide_updates, 147 | outstream=outstream, 148 | ) 149 | 150 | if not source or len(source) == 0: 151 | # No source directory is provided, get the user to choose a teamdrive 152 | source = drive_handler.select_teamdrive() 153 | 154 | if not hide_updates: 155 | typer.secho( 156 | f"Walking through `{drive_handler.drive_name(source)}`\n", 157 | fg=typer.colors.GREEN, 158 | err=True, 159 | ) 160 | 161 | __check_collisions( 162 | force=force, 163 | dst=join_path( 164 | destination, 165 | root_name if root_name else drive_handler.drive_name(source), 166 | ), 167 | ) 168 | 169 | drive_handler.walk( 170 | source=source, 171 | change_dir=file_handler.switch_dir, 172 | generator=file_handler.strm_generator, 173 | orig_path=destination, 174 | custom_root=root_name, 175 | ) 176 | 177 | typer.secho( 178 | f"Completed generating strm files\nFiles generated in: {destination}", 179 | fg=typer.colors.GREEN, 180 | ) 181 | 182 | 183 | def main(): 184 | typer.run(cmd_interface) 185 | -------------------------------------------------------------------------------- /kodi_strm/file_handler.py: -------------------------------------------------------------------------------- 1 | from os import mkdir 2 | from os.path import exists as path_exists 3 | from os.path import join as join_path 4 | from os.path import splitext 5 | from typing import Optional 6 | 7 | import typer 8 | from reprint import output 9 | 10 | 11 | class FileHandler: 12 | def __init__( 13 | self, 14 | destination: str, 15 | include_extensions: bool, 16 | live_updates: bool, 17 | outstream: output = None, 18 | ) -> None: 19 | self.__cur_path: str = destination 20 | self.__cur_dir: str = None 21 | self.__cur_file: str = None 22 | 23 | self.__directories: int = 0 24 | self.__files: int = 0 25 | self.__skipped: int = 0 26 | self.__size: int = 0 27 | 28 | self.__live_updates = live_updates 29 | self.__include_ext = include_extensions 30 | 31 | self.__outstream = outstream 32 | 33 | @staticmethod 34 | def __readable_size(size: int) -> str: 35 | """ 36 | Converts number of bytes into readable format, and returns the same as string 37 | """ 38 | 39 | # An array size units. Will be used to convert raw size into a readable format. 40 | sizes = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"] 41 | 42 | counter = 0 43 | while size >= 1024: 44 | size /= 1024 45 | counter += 1 46 | 47 | return "{:.3f} {}".format(size, sizes[counter]) 48 | 49 | @staticmethod 50 | def __is_media_file(file_name: str, mime_type: str) -> bool: 51 | """ 52 | Decides if a file is a media file -- used to decide which files to create 53 | `.strm` file for 54 | 55 | Params 56 | ------- 57 | file_name: Name of the file. Used to identify media files from their extension 58 | mime_type: Mime type of the file on google drive 59 | 60 | Returns 61 | -------- 62 | Boolean indicating if the file is a media file, or not 63 | """ 64 | 65 | if "video" in mime_type: 66 | return True 67 | elif file_name.endswith( 68 | ( 69 | ".mp4", 70 | ".mkv", 71 | ) 72 | ): 73 | # Skip if item doesn't have media-file extension, or mime type 74 | return True 75 | 76 | return False 77 | 78 | @staticmethod 79 | def __shrink(input: str, *, max_len: int = 60) -> str: 80 | """ 81 | Shrinks the string to fit into a fixed number of characters 82 | 83 | Remarks 84 | -------- 85 | Shortens string to fit `max_len` characters by replacing with period(s) [...] 86 | 87 | For example, the string 88 | `This is a long string` 89 | 90 | When shrunk to 10 max characters using this method, will be 91 | `Thi....ing` 92 | 93 | Returns 94 | -------- 95 | String containing `input` string shrunk to fit within `max_len` charcters 96 | """ 97 | 98 | if len(input) <= max_len: 99 | return input 100 | 101 | # Leave space for 4 period(s) - two on each side, divide rest characters in two 102 | half_len = int((max_len / 2) - 2) 103 | return f"{input[:half_len]}....{input[-half_len:]}" 104 | 105 | def __update(self): 106 | """ 107 | Prints updates to the screen 108 | """ 109 | 110 | if not self.__live_updates: 111 | return # direct return 112 | 113 | max_len = 75 114 | 115 | if self.__cur_dir: 116 | self.__outstream[0] = typer.style( 117 | self.__shrink(f"Scanning directory: {self.__cur_dir}", max_len=max_len), 118 | fg=typer.colors.GREEN, 119 | ) 120 | 121 | self.__outstream[1] = "\n" 122 | if self.__cur_file: 123 | self.__outstream[2] = self.__shrink(self.__cur_file, max_len=max_len) 124 | self.__outstream[3] = "\n" 125 | 126 | self.__outstream[4] = f"Directories Scanned: {self.__directories}" 127 | self.__outstream[5] = f"Files Scanned: {self.__files}" 128 | self.__outstream[6] = f"Bytes Scanned: {self.__readable_size(self.__size)}" 129 | self.__outstream[7] = f"Files Skipped: {self.__skipped}" 130 | self.__outstream[8] = "\n" 131 | 132 | def __create_strm( 133 | self, 134 | item_id: str, 135 | item_name: str, 136 | drive_id: Optional[str], 137 | td_id: Optional[str], 138 | ) -> bool: 139 | """ 140 | Creates `.strm` file for files using their ID 141 | 142 | Params 143 | ------- 144 | item_id: ID of the item on Google Drive 145 | item_name: Name of the item - as on Drive 146 | drive_id: Optional, ID of the drive containing the item 147 | td_id: Optional, ID of teamdrive containing the item. For items in a teamdrive 148 | """ 149 | 150 | # The hard-coded strings are simply how the `Drive Add-on` extension expects 151 | # them to be 152 | file_contents: str = ( 153 | f"plugin://plugin.googledrive/?action=play&item_id={item_id}" 154 | ) 155 | 156 | if td_id: 157 | file_contents += f"&item_driveid={td_id}" 158 | if drive_id: 159 | file_contents += f"&driveid={drive_id}" 160 | 161 | file_name = ( 162 | f"{item_name}.strm" 163 | if self.__include_ext 164 | else f"{splitext(item_name)[0]}.strm" # remove extension if not needed 165 | ) 166 | 167 | # Create strm file, and write to it 168 | with open(join_path(self.__cur_path, file_name), "w+") as f: 169 | f.write(file_contents) 170 | 171 | return True 172 | 173 | def switch_dir(self, path: str, dir_name: str): 174 | if not path_exists(path): 175 | mkdir(path=path) 176 | self.__directories += 1 177 | 178 | self.__cur_path = path 179 | self.__cur_dir = dir_name 180 | 181 | def strm_generator( 182 | self, 183 | item_id: str, 184 | item_name: str, 185 | mime_type: str, 186 | item_size: int, 187 | drive_id: Optional[str], 188 | td_id: Optional[str], 189 | ): 190 | """ 191 | Internally creates `.strm` files -- ignores non-media files 192 | """ 193 | 194 | self.__cur_file = item_name 195 | 196 | # Check if the file is a media file -- if not, direct return 197 | if not self.__is_media_file(file_name=item_name, mime_type=mime_type): 198 | self.__skipped += 1 # calculate this as a `skipped` file 199 | self.__update() 200 | return 201 | 202 | result = self.__create_strm( 203 | item_id=item_id, 204 | item_name=item_name, 205 | drive_id=drive_id, 206 | td_id=td_id, 207 | ) 208 | 209 | if result: 210 | self.__size += item_size 211 | self.__files += 1 212 | self.__update() 213 | -------------------------------------------------------------------------------- /kodi_strm/drive_handler.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | from os.path import exists as path_exists 3 | from os.path import join as join_path 4 | from pickle import dump as dump_pickle 5 | from pickle import load as load_pickle 6 | from typing import Any, Callable, Dict, List, Optional, Tuple 7 | 8 | import googleapiclient 9 | import googleapiclient.discovery as discovery 10 | import typer 11 | from google.auth.transport.requests import Request 12 | from google.oauth2.credentials import Credentials 13 | from google_auth_oauthlib.flow import InstalledAppFlow 14 | 15 | 16 | class DriveHandler: 17 | """ 18 | Deals with Drive API and related stuff 19 | """ 20 | 21 | def __init__(self): 22 | self.resource: googleapiclient.discovery.Resource = self.__authenticate() 23 | 24 | # Dictionary mapping ID's to their (human-readable) name. Acts as a simple cache 25 | # to reduce API calls. Can be used for teamdrives, and normal directories 26 | self.dirs: Dict[str, str] = {} 27 | 28 | def __authenticate(self) -> discovery.Resource: 29 | """ 30 | Authenticates user session using Drive API. 31 | 32 | Remarks 33 | -------- 34 | Attempts to open a browser window asking user to login and grant permissions 35 | during the first run. Saves a `.pickle` file to skip this step in future runs 36 | 37 | Returns 38 | -------- 39 | Object of `googleapiclient.discovery.Resource` 40 | """ 41 | 42 | creds: Optional[Credentials] = None 43 | 44 | # Selectively asks for read-only permission 45 | SCOPES = ["https://www.googleapis.com/auth/drive.readonly"] 46 | 47 | if path_exists("token.pickle"): 48 | with open("token.pickle", "rb") as token: 49 | creds: Credentials = load_pickle(token) 50 | 51 | if not creds or not creds.valid: 52 | if creds and creds.expired and creds.refresh_token: 53 | creds.refresh(Request()) 54 | else: 55 | flow = InstalledAppFlow.from_client_secrets_file( 56 | "credentials.json", SCOPES 57 | ) 58 | creds = flow.run_local_server(port=0) 59 | with open("token.pickle", "wb") as token: 60 | dump_pickle(creds, token) # save credentials for next run 61 | 62 | return googleapiclient.discovery.build("drive", "v3", credentials=creds) 63 | 64 | def __get_teamdrives(self) -> Dict[str, str]: 65 | """ 66 | Fetches and returns a list of all teamdrives associated with the Google account 67 | 68 | Remarks 69 | -------- 70 | Returns a dictionary of all teamdrives associated with the account. Aborts if 71 | the account has no teamdrive associated with it. 72 | 73 | Teamdrives (if found) will be cached in `self.dirs` 74 | """ 75 | 76 | next_page_token: Optional[str] = None 77 | first_run: bool = True 78 | 79 | tds: Dict[str, str] = {} 80 | while next_page_token or first_run: 81 | first_run = False 82 | page_content: Dict[str, Any] = ( 83 | self.resource.drives() 84 | .list(pageSize=100, pageToken=next_page_token) 85 | .execute() 86 | ) 87 | 88 | for item in page_content["drives"]: 89 | self.dirs[item["id"]] = item["name"] 90 | tds[item["id"]] = item["name"] 91 | 92 | # Loops as long as there is a `nextPageToken` 93 | next_page_token = page_content.get("nextPageToken", None) 94 | 95 | return tds 96 | 97 | def drive_name(self, drive_id: str) -> str: 98 | """ 99 | Returns name for a teamdrive using its ID 100 | 101 | Remarks 102 | -------- 103 | Looks for info in local cache - if not found, will internally make a network 104 | call and return the directory name 105 | """ 106 | 107 | if drive_id not in self.dirs: 108 | self.fetch_dir_name(dir_id=drive_id) # fetch info if not cached already 109 | 110 | return self.dirs[drive_id] 111 | 112 | def fetch_dir_name(self, *, dir_id: str) -> str: 113 | """ 114 | Returns info obtained regarding a directory/file from Google Drive API 115 | 116 | Remarks 117 | -------- 118 | Designed to get directory name from drive API, but can work with files as well. 119 | Internally caches directory name against folder id in `self.dirs` 120 | 121 | Always fetches info through a network call. Use `drive_name` to look through 122 | cache before making a network call 123 | """ 124 | 125 | try: 126 | result = ( 127 | self.resource.files() 128 | .get(fileId=dir_id, supportsAllDrives=True) 129 | .execute() 130 | ) 131 | 132 | if result.get("id", True) == result.get("teamDriveId", None): 133 | # Enters this block only if the `dir_id` belongs to a teamdrive 134 | result = self.resource.drives().get(driveId=dir_id).execute() 135 | 136 | # Cache directory name -- works with teamdrives and folders, id's are unique 137 | self.dirs[result["id"]] = result["name"] 138 | return result["name"] 139 | except Exception as e: 140 | typer.secho( 141 | f"Unable to find drive directory `{dir_id}`", fg=typer.colors.RED 142 | ) 143 | raise typer.Abort() 144 | 145 | def select_teamdrive(self) -> str: 146 | """ 147 | Gets user to select source drive from team-drives 148 | 149 | Returns 150 | -------- 151 | String containing ID of the selected teamdrive 152 | """ 153 | 154 | tds = self.__get_teamdrives() 155 | 156 | if len(tds) == 0: 157 | typer.secho("Unable to locate any teamdrives!", fg=typer.colors.RED) 158 | raise typer.Abort() 159 | 160 | counter: int = 1 161 | keys: List[str] = list(tds.keys()) 162 | for id in keys: 163 | typer.secho( 164 | f" {counter}. " + ("/" if counter % 2 else "\\") + f"\t{tds[id]}", 165 | fg=typer.colors.GREEN if counter % 2 else typer.colors.CYAN, 166 | ) 167 | 168 | counter += 1 169 | 170 | while True: 171 | typer.echo("\nSelect a teamdrive \nteamdrive> ", err=True, nl=False) 172 | choice: str = input().strip() 173 | 174 | try: 175 | selected: int = int(choice) 176 | if selected > len(tds): 177 | raise AssertionError(f"Expected input in range [1-{len(tds)}]") 178 | return keys[selected - 1] 179 | except AssertionError as e: 180 | typer.secho(f"\t{type(e).__name__}: {e}", fg=typer.colors.RED, err=True) 181 | except ValueError as e: 182 | typer.secho( 183 | f"\tValueError: Invalid input `{choice}`", 184 | fg=typer.colors.RED, 185 | err=True, 186 | ) 187 | 188 | def walk( 189 | self, 190 | source: str, 191 | *, 192 | orig_path: str, 193 | change_dir: Callable[[str], None], 194 | generator: Callable[[str, str, str, int, Optional[str], Optional[str]], None], 195 | custom_root: Optional[str] = None, 196 | ): 197 | """ 198 | Walks through the source folder in Google Drive - creating `.strm` files for 199 | each media file encountered 200 | 201 | Params 202 | ------- 203 | source: String containing ID of the source directory 204 | orig_path: String containing path to the destination directory 205 | change_dir: Method call to create, and change directories. Should accept 206 | complete path to the directory as parameter 207 | generator: Method call to create `strm` files 208 | strm_creator: Function to create `.strm` files - supplied arguments; item id, 209 | item name, teamdrive id [optional] 210 | custom_root: Optional. String containing custom name for root directory 211 | """ 212 | 213 | if not custom_root and not self.dirs.get(source, False): 214 | # The source directory has not been cached, fetch the same 215 | self.fetch_dir_name(dir_id=source) 216 | 217 | # Stack to track directories encountered. Each entry in the stack will be a 218 | # tuple consisting of the directory id, and a path to the local directory, 219 | # and a string containing the name of the directory 220 | queue: deque[Tuple[str, str]] = deque() 221 | queue.append( 222 | [ 223 | source, 224 | join_path(orig_path, custom_root if custom_root else self.dirs[source]), 225 | self.dirs[source], 226 | ] 227 | ) 228 | 229 | page_token: str = None 230 | while len(queue): 231 | 232 | dir_id, path, dir_name = queue.pop() 233 | change_dir(path, dir_name) 234 | 235 | page = ( 236 | self.resource.files() 237 | .list( 238 | pageSize=1000, # get max items possible with each call 239 | pageToken=page_token, # decides page for pagination 240 | fields="files(name, id, mimeType, teamDriveId, driveId, size)", 241 | supportsAllDrives=True, # enable support for teamdrives 242 | includeItemsFromAllDrives=True, 243 | # Ensure items are in parent directory, exclude deleted items 244 | q=f"'{dir_id}' in parents and trashed=false", 245 | ) 246 | .execute() 247 | ) 248 | 249 | for item in page["files"]: 250 | if item["mimeType"] == "application/vnd.google-apps.folder": 251 | # Add this directory to the queue 252 | queue.append( 253 | [item["id"], join_path(path, item["name"]), item["name"]] 254 | ) 255 | continue 256 | 257 | # Generate STRM file if the flow-of-control reaches this point 258 | # The function will internally ignore non-media files 259 | generator( 260 | item_id=item["id"], 261 | item_name=item["name"], 262 | mime_type=item["mimeType"], 263 | item_size=int(item["size"]), 264 | drive_id=item.get("driveId", None), 265 | td_id=item.get("teamDriveId", None), 266 | ) 267 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## kodi-strm 2 | 3 |
4 | 5 | ![A gif displaying `kodi-strm` in action][demo-gif] 6 | 7 | ![Release][latest-release] 8 | ![Release Date][release-date] 9 | ![Language][language] 10 | ![License][license] 11 | ![Code Size][code-size] 12 | 13 | A project to complement the Google Drive AddOn for Kodi 14 | 15 |

16 | Jump to Docs » 17 |

18 | · 19 | Bug Report 20 | · 21 | Request Feature 22 | · 23 | Fork Repo 24 | · 25 |

26 | 27 |
28 | 29 | 30 | ## About The Project 31 | 32 | A python script to complement the functionality of [Google Drive AddOn][kodi-addon] for Kodi. 33 | 34 | While the add-on [claims][speed-claim] to be "*extremely fast*", I found this claim to 35 | be dubious. Based on my usage so far, the addon was slow, unreliable, crashed frequently, 36 | and often got stuck. Eventually, frustration drove me to write a simple python script to 37 | generate strm files for the add-on, post which all the add-on had to do was simply play 38 | them! Over time, the small script grew into this project. 39 | 40 | To reiterate, the sole purpose of this project is to create strm files for media file(s) 41 | present in a directory on Google Drive — and achieve this more reliably than the than 42 | the existing Kodi add-on. 43 | 44 | No ill-will is intended towards Carlos (the creator of the Google Drive add-on), I am 45 | extremely thankful for all the work that went into making the add-on — *kodi-strm* is 46 | in no way meant to be a replacement for the Google Drive add-on. 47 | 48 | ### What is a strm file? 49 | 50 | In simple terms, *strm* files refer to files having an extension of `.strm`. They are 51 | created by multimedia applications (Kodi and Plex being the main examples) and, primarily 52 | contain a URL pointing to the media file(s) stored in a remote server/cloud, 53 | 54 | These files are then parsed by the application, post which the URL is used to stream the 55 | actual media file. 56 | 57 | ## Pre-Requisites 58 | * Python 3.9 or above — get it from [here][python-dl] 59 | 60 | ## Setup 61 | 1. Clone this project 62 | 2. Create a [Google Project][google-console] and [enable Google Drive API][enable-api] 63 | 3. Setup credentials to use this API 64 | 4. Download these credentials as a JSON file, rename this file to be `credentials.json` 65 | 5. Move `credentials.json` into this project directory 66 | 6. Install the project dependencies. 67 | 68 | ```shell 69 | pip install -r requirements.txt 70 | ``` 71 | 72 | Alternatively, for the last step, you can directly use the `setup.sh` or `setup.bat` 73 | scripts and have them create an efficient setup for you. 74 | 75 | #### What do the setup scripts do? 76 | 77 | The setup script will; 78 | * Update `pip` 79 | * Install `virtualenv` 80 | * Create a virtual environment named `venv` 81 | * Activate this environment 82 | * Install required dependencies into this virtual environment 83 | 84 | While anyone is free to use the direct setup scripts, do note that they are primarily 85 | for my use (to quickly setup a system for testing/debugging) - and *aren't guaranteed* 86 | to work in your system. 87 | 88 | ### Post Setup 89 | 90 | Once you're done with setting up a working enviroment, simply run the script 91 | 92 | ```sh 93 | python strm-generator.py 94 | 95 | OR 96 | 97 | python -m kodi_strm 98 | ``` 99 | 100 | For the first run, the script will ask you to login to a Google account. While not 101 | necessary, it is recommended to use the same Google account for this script, and the 102 | Google Drive Add-On. 103 | 104 | P.S. Make sure to install the [Google Drive add-on][kodi-addon] in your Kodi installation. 105 | 106 | ## Usage 107 | 108 | Running the script without any arguments will fetch a list of all teamdrives to which 109 | the account has access, allowing you to select a teamdrive from this list. The selected 110 | teamdrive will then be treated as the source directory for the script. 111 | 112 | Alternatively, you can use [custom arguments](#custom-arguments) to modify the source 113 | directory, destination, or more! 114 | 115 | Once the script finishes generating strm files after a scan, add the resulting 116 | [directory as a source](https://kodi.wiki/view/Adding_video_sources) in your Kodi 117 | installation. From there on, the Google Drive AddOn will treat these `.strm` files as 118 | actual media files and will be able to scan/play them normally. 119 | 120 | > Note: 121 | > 122 | > If you are unable to play the media files with Kodi, make sure that you have installed 123 | > the [Kodi AddOn for Google Drive][kodi-addon] and have logged into the add-on with the 124 | > **same** Google Account that you are using for this script. 125 | 126 | Once you're comfortable with the usage of this script, you might want to take a look at 127 | the [advanced setup](#advanced-setup) section for some ideas :) 128 | 129 | ### Quick Overview 130 | 131 | | Flag | ShortHand | Description | Default | 132 | |:-----------------:|:----------:|:---------------------------------------------------------:|:--------------------------:| 133 | | `--version` | `-v` | Display `kodi-strm` version and exit | NA | 134 | | `--help` | | Display help and exit | NA | 135 | | `--source` | | Drive ID for the source folder | NA | 136 | | `--destination` | `--dest` | Destination directory where `.strm` files are generated | Current Working Directory | 137 | | `--rootname` | `--root` | Name of the `source` directory | Name of `source` directory | 138 | | `--no-extensions` | `--no-ext` | Remove original file extensions from generated strm files | NA | 139 | | `--no-updates` | | Disable live updates on the screen | NA | 140 | | `--force` | `-f` | Directly wipe out `root` directory in case of collision | NA | 141 | 142 | By default, the strm files generated after a scan are stored in the **working directory**. 143 | Use `pwd` in Unix-based systems, or `cd` in Windows get the location of current working 144 | directory. 145 | 146 | Note: Take a look at the flag for [custom destination directory](#custom-destination-directory) 147 | and to modify this behaviour. 148 | 149 | ## Custom Arguments 150 | 151 | Custom arguments passed when executing the script from the terminal allow you to modify 152 | the behaviour of the script! 153 | 154 | ```sh 155 | python strm-generator.py [OPTIONS] 156 | 157 | OR 158 | 159 | python -m kodi_strm [OPTIONS] 160 | ``` 161 | 162 | #### Source Directory 163 | 164 | **Flag:** `--source=`
165 | **Shorthand:** `NA`
166 | **Value Expected:** ID of a folder/teamdrive on Google Drive 167 | 168 | This flag allows you to scan the contents of a folder/teamdrive from Google Drive, i.e. 169 | with the help of this flag instead of scanning a complete teamdrive (or the main drive), 170 | an individual folder can be selectively scanned using its ID. 171 | 172 | > A source directory is a folder/teamdrive in Google Drive that is used as the **source** 173 | > to generate strm files by *kodi-strm* 174 | 175 | If source directory is **not** specified, the script will ask you to select a teamdrive 176 | as the source - the selected teamdrive will be scanned by the script. 177 | 178 | > Note: Check [Getting Folder IDs](#getting-folder-ids) section to learn how to get the 179 | > ID of a folder/teamdrive from Google Drive. 180 | 181 | #### Custom Destination Directory 182 | 183 | **Flag:** `--destination=""`
184 | **Shorthand:** `--dest`
185 | **Expected Value:** Path to an ***existing*** directory.
186 | 187 | This flag is used to decide the destination directory in which the root directory 188 | (containing the strm files) will be placed. Should be a path to an existing directory! 189 | 190 | [>> Resource: Destination Directory vs Root Directory](#destination-directory-vs-root-directory) 191 | 192 | P.S. If the path contains spaces, wrap it in double quotes. 193 | 194 | #### Updates 195 | 196 | **Flag:** `--no-updates`
197 | **Shorthand:** `NA`
198 | **Expected Value:** `NA`
199 | 200 | Obviously enough, using this flag disables any updates from *kodi-strm* (expect for the 201 | final completion message). 202 | 203 | By default, *kodi-strm* prints realtime updates on the terminal - this includes info 204 | on the directory and file(s) being scanned, and more! 205 | 206 | Having realtime updates might not always be desirable (especially if run in background). 207 | Using this flag will run the script in silent mode - the only console message will be to 208 | completion. 209 | 210 | #### Custom Name for Root Directory 211 | 212 | **Flag:** `--rootname=`
213 | **Shorthand:** `--root`
214 | **Expected Value:** Name for the root directory
215 | 216 | Optional flag to modify the name of root directory. By default, the name of the 217 | teamdrive/folder being scanned will be used as the name of the root directory. 218 | 219 | [>> Resource: Destination Directory vs Root Directory](#destination-directory-vs-root-directory) 220 | 221 | Important: If a directory with the same name as the root directory already exists inside 222 | destination directory, the script will ask you for confirmation before proceeding to 223 | wipe the existing path. 224 | 225 | #### Force-Wipe Existing Paths 226 | 227 | **Flag:** `--force`
228 | **Shorthand:** `-f`
229 | **Expected Value:** `NA`
230 | 231 | With the `force` flag enabled, if there is a collision when creating the `root` 232 | directory, the existing path will be wiped completely - without any confirmation! 233 | 234 | By default, in case of a path collision, *kodi-strm* will ask you for confirmation 235 | before deleting the existing path. However, this may not always be desired - especially 236 | if you already plan to overwrite the existing directory. 237 | 238 | For such *specific* scenarios, enabling the `force` flag ensures *kodi-strm* will 239 | directly proceed by wiping the existing path (without asking for a confirmation). 240 | 241 | #### Version 242 | 243 | **Flag:** `--version`
244 | **Shorthand:** `-v`
245 | **Expected Value:** `NA`
246 | 247 | Prints info related to the operating system, and the current version of *kodi-strm*, 248 | following which the script directly terminates 249 | 250 | #### Help 251 | 252 | **Flag:** `--help`
253 | **Shorthand:** `NA`
254 | **Expected Value:** `NA`
255 | 256 | Prints a list of all flags available, and a brief description regarding them 257 | 258 | ## Resources 259 | 260 | #### Getting Folder ID's 261 | 262 | You can get the id for a particular folder from the url to the directory. 263 | 264 | For example, in the URL `https://drive.google.com/drive/folders/0AOC6NXsE2KJMUk9PTA`, 265 | the folder-id is `0AOC6NXsE2KJMUk9PTA`. 266 | 267 | This method can also be used to get the ID of a particular TeamDrive and use it as a 268 | [custom argument](#scanning-a-folder-selectively) to directly scan the teamdrive. 269 | 270 | 271 | #### Destination Directory vs Root Directory 272 | 273 | In simplifed terms, `destination` directory is "destination" directory, which will 274 | contain the *root* directory. When run, `kodi-strm` will create a `root` directory 275 | in the destination directory - this *root* directory will contain the contents of 276 | scanned Google Drive folder. 277 | 278 | The `root` directory will be the directory/teamdrive that is being scanned from Google 279 | Drive. As an example, if you scan a Google Drive folder *Movies* with folder ID 280 | `0TRC6NXsE2KJMUkasdA` and want to store the generated *strm* files in `~/Downloads`; 281 | - The directory `~/Downloads` will be the `destination` directory 282 | - Inside `~/Downloads`, *kodi-strm* will **create** a directory named "Movies". This 283 | is the `root` directory - that is placed inside the `destination` directory. 284 | 285 | You can modify destination path (the `destination` directory) with the`--destination` 286 | flag, at the same time, you can modify the name of the created directory ("Movies") to 287 | be, say "*Drive Movies*" with the `--rootname` flag! 288 | 289 | The following command will use `~/home` as the destination directory - i.e. strm files 290 | will be placed at this location, and "*output files*" as the name of source 291 | root directory that will contain the actual strm files 292 | 293 | ```sh 294 | python strm-generator.py --source=0TRC6NXsE2KJMUkasdA --destination="~/home" --rootname="output files" 295 | ``` 296 | 297 | From the example above, the folder `0TRC6NXsE2KJMUkasdA` is named "Movies" on Google 298 | Drive, but running the above command will rename the root folder to be "output files" 299 | in the *destination* directory. 300 | 301 | P.S. The `destination` path should point to an **existing directory**, while the 302 | `rootname` will be a folder that is **created by the script in destination directory**, 303 | and should **not** exist. 304 | 305 | ## Examples 306 | 307 | #### Scanning a folder selectively 308 | 309 | Simply scan a Google drive folder with the ID `0AOC6NXsE2KJMUk9PVA` and place the root 310 | directory at the destination `/home/kodi library` 311 | 312 | ```sh 313 | python -m kodi_strm --source=0AOC6NXsE2KJMUk9PVA --destination="/home/kodi library" 314 | ``` 315 | 316 | Note the additional destination flag in the command, instead of placing the results in 317 | the working directory, the script will now place them inside `/home/kodi library`. 318 | 319 | Also, since the destination path contains a space, it needs to be wrapped in quotes. It 320 | is recommended to wrap paths in double quotes regardless of whether it contains spaces. 321 | 322 | #### Custom root directory 323 | 324 | Running the following command will rename the root directory to be `new root directory` 325 | 326 | ```sh 327 | python strm-generator.py --source=0AOC6NXsE2KJMUk9PVA --dest="/home/kodi library" --rootname="new root directory" 328 | ``` 329 | 330 | The strm files will inside a directory named `new root directory` (the root directory). 331 | The root directory by itself will be present in `"/home/kodi library"` (the destination 332 | directory). 333 | 334 | ### Miscellaneous Examples 335 | 336 | * Scanning a particular folder on drive, with custom destination and root directories, 337 | plus no updates to the console (using all the flags at once). 338 | 339 | ```sh 340 | python -m kodi_strm --source=0AOC6NXsE2KJMUk9PVA --dest="/home/kodi libraries" --rootname="Staging; Media" --no-updates 341 | ``` 342 | 343 | * Scanning a folder with no updates 344 | ```sh 345 | python strm-generator.py --source=0AOC6NXsE2KJMUk9PVA --no-updates 346 | ``` 347 | 348 | * Scanning a folder selectively with custom root directory 349 | ```sh 350 | python strm-generator.py --source=0AOC6NXsE2KJMUk9PVA --rootname="Media" 351 | ``` 352 | 353 | * Scanning a folder with custom destination directory and no updates on console 354 | ```sh 355 | python strm-generator.py --source=0AOC6NXsE2KJMUk9PVA --dest="/home/Videos" --no-updates 356 | ``` 357 | 358 | ## Advanced Setup 359 | 360 | This section contains ideas and suggestions to how you can use this script to get a 361 | seamless experience with you Kodi setup. 362 | 363 | With a little help of [custom arguments](#custom-arguments) supported by the script, 364 | you can set up the script to be run on fixed intervals (with pre-fixed parameters). 365 | 366 | For example, I'm on Linux, I've setup a [systemd.service][systemd] to run this script 367 | once every couple of days. At every run, the script scans a folder from Google Drive, 368 | and places it inside an existing Kodi library. 369 | 370 | Whenever I open up Kodi, it automatically scrapes the new files, and adds them to my 371 | library. And so, my system automatically fetches any updates on Google Drive, syncs 372 | them, and adds them to Kodi without requiring any input from me. 373 | 374 | If needed, the systemd service can be modified to run the script multiple times, 375 | scanning different sources each time to be able to scale my current setup to span 376 | across multiple teamdrives/folders, all this without requiring any sort of input. 377 | 378 | Windows users can achieve the same functionality as systemd.service using 379 | [Windows Task Scheduler](https://en.wikipedia.org/wiki/Windows_Task_Scheduler). In case 380 | someone wants to replicate a similar setup as mine, here are some useful links to help 381 | you get started. 382 | 383 | - [Creating a systemd.service](https://medium.com/@benmorel/creating-a-linux-service-with-systemd-611b5c8b91d6) 384 | - [Using Windows Task Scheduler](https://windowsreport.com/schedule-tasks-windows-10) 385 | - [Setting up Kodi for auto-scan](https://www.howtogeek.com/196025/ask-htg-how-do-you-set-your-xbmc-library-to-automatically-update/) 386 | 387 | P.S. If you're setting up a service for automated runs, you might want to add the 388 | `--force` flag to your command ([docs](#force-wipe-existing-paths)) - especially for 389 | setups similar to mine! The flag ensures *kodi-strm* will wipe the existing 390 | directory instead of (permanently) waiting for a confirmation in the background. 391 | 392 | 393 | ## Roadmap 394 | 395 | The main aim for this project isn't to replace the exising Google Drive AddOn, but to build upon it and fix the flaws. As such, a large part of what I found missing in the add-on has already been fixed -- namely, speed and reliablitly. 396 | 397 | Based on the tests I've run, this script is ~20% faster than the Google Drive Add-On, and is a lot more reliable (no crashes so far). 398 | 399 | > *For the curious, yes, I ran the test on the exact same source without any modifications. The results are as accurate as possible.* 400 | 401 | #### A list of *possible* improvements; 402 | - ~~Display live progress when the script runs~~ added in #1 403 | - Multithreading (still not sure on this one) - despite GIL, the execution speed can be improved to *some* extent 404 | 405 | ## Contributions 406 | 407 | Contributions are what make the open source community such an amazing place to learn, 408 | inspire, and create. Any contributions made are **extremely appreciated**. 409 | 410 | 1. Fork the Project 411 | 2. Create your Feature Branch (`git checkout -b feature/new-feature`) 412 | 3. Commit your Changes (`git commit -m 'Add amazing new features'`) 413 | 4. Push to the Branch (`git push origin feature/new-feature`) 414 | 5. Open a Pull Request 415 | 416 | ## License 417 | Distributed under the MIT License. See [`LICENSE`](./LICENSE). for more information. 418 | 419 | ## Acknowledgements 420 | * [Img Shields](https://shields.io) 421 | * [Google API Python Client](https://github.com/googleapis/google-api-python-client) 422 | * [Coloroma](https://github.com/tartley/colorama) 423 | * [Reprint](https://github.com/Yinzo/reprint) 424 | 425 | ### Sponsors 426 | 427 | `kodi-strm` was made possible with the support of the following organization(s): 428 | 429 |
430 | 431 | 432 | 433 |
434 | 435 | [python-dl]: https://www.python.org/downloads 436 | [google-console]: http://console.developers.google.com 437 | [kodi-addon]: https://kodi.tv/addons/matrix/plugin.googledrive 438 | [systemd]: https://man7.org/linux/man-pages/man5/systemd.service.5.html 439 | [enable-api]: https://developers.google.com/drive/api/v3/enable-drive-api 440 | [speed-claim]: https://github.com/cguZZman/plugin.googledrive#:~:text=Extremely%20fast 441 | [code-size]: https://img.shields.io/github/languages/code-size/notsatan/kodi-strm?style=for-the-badge 442 | [language]: https://img.shields.io/github/languages/top/notsatan/kodi-strm?style=for-the-badge 443 | [license]: https://img.shields.io/github/license/notsatan/kodi-strm?style=for-the-badge 444 | [latest-release]: https://img.shields.io/github/v/release/notsatan/kodi-strm?style=for-the-badge 445 | [release-date]: https://img.shields.io/github/release-date/notsatan/kodi-strm?style=for-the-badge 446 | [issues-url]: https://img.shields.io/github/issues-raw/notsatan/kodi-strm?style=for-the-badge 447 | [demo-gif]: https://user-images.githubusercontent.com/22884507/177959717-33ed1a20-c289-4dcf-a9b2-4d497e4eb2c6.gif --------------------------------------------------------------------------------