├── Diagram.png ├── LICENSE.txt ├── README.md ├── _config.yml ├── file_operations.py ├── hns_operations.py ├── requirements.txt ├── rss_operations.py ├── skynet_uploader.gif └── uploader.py /Diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t-900-a/emby-skynet-uploader/cfaa4a6208ef75337dcfa2d9b163ccf79250af53/Diagram.png -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, t-900-a 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Script for uploading to skynet 2 | =============================== 3 | 4 | Script to upload media files from a ~~jellyfin~~ or emby server to skynet with support for handshake name service via namebase. 5 | 6 | An RSS feed can be generated containing sia links to all your media. The RSS feed can also be uploaded to skynet and your namebase domain entry updated so that your custom domain can point to your latest rss feed. 7 | 8 | Example: `https://skynet.net/hns/` 9 | 10 | Live Example: https://siasky.net/hns/01347/ 11 | 12 | * release 0.2 13 | * open source: https://github.com/t-900-a/jellyfin-skynet-uploader 14 | * Jellyfin is the FREE & OPEN SOURCE media player 15 | * works with Emby 4.* 16 | * Python 3.x compatible 17 | * What is skynet? 18 | * * It's a decentralized CDN: https://siasky.net/ 19 | 20 | ![Demo run through](skynet_uploader.gif) 21 | 22 | ![Diagram](https://raw.githubusercontent.com/t-900-a/jellyfin-skynet-uploader/master/Diagram.png) 23 | 24 | Note that all examples are creative commons or open source movies 25 | 26 | Copyrights 27 | ---------- 28 | 29 | Released under the BSD 3-Clause License. See `LICENSE.txt`_. 30 | 31 | Copyright (c) 2020 t-900-a 32 | 33 | Want to help? 34 | ------------- 35 | 36 | Merge requests are invited 37 | 38 | There are many todo's in the code 39 | 40 | TODO: Stream the download from the emby server to get around the requirement to have download permissions on the emby server 41 | TODO: Include additional information into the rss feed i.e. genre, actors, imdb rating, etc 42 | 43 | Available Scripts 44 | ----------------- 45 | * uploader.py : Media Uploader 46 | * * Description: Script used to upload your media library items to skynet to share with the world 47 | 48 | Parameters 49 | ---------- 50 | * --datecreated 51 | * * iso-8601 date format i.e. 2020-03-08 52 | * * all media that was created on or after that date will be uploaded to skynet 53 | * --itemid 54 | * * the itemid from emby that you would like to upload to skynet 55 | * --all 56 | * * include this argument if you would like to upload all media found on the server to skynet 57 | * --mediatype 58 | * * i.e. Movie 59 | * * default: Movie,Episode 60 | * * specify the type of media from the media server you would like to upload" 61 | * --mediaserverconfig 62 | * * config file for the emby server connection 63 | * * script will prompt you via the command line for your current media server information (IP/PORT/ADMIN user) 64 | * * file will be generated if none exists 65 | 66 | * RSS related parameters 67 | * * --rss_id 68 | * * * Id for your channel 69 | * * * default: random characters 70 | * * --rss_title 71 | * * * The name of your rss channel, if none is specified an rss feed will not be generated 72 | * * * i.e. anon's Media 73 | * * * Only required parameter to produce a rss atom feed 74 | * * --rss_link 75 | * * * Include a link if you would like your site to be referenced 76 | * * * i.e. https://www.mysharedmedia.com/emby/ 77 | * * --rss_description 78 | * * * Choose the description of the feed 79 | * * --rss_contributor 80 | * * * Choose the description of the feed 81 | * * --rss_subtitle 82 | * * * Addition comment for your site if you want it 83 | * * * i.e. For more content, please donate _cryptocurrency_symbol to _cryptocurrency_address 84 | 85 | * Skynet (Siacoin) and Namebase (HNS) parameters 86 | * * --skynet_file_size_limit 87 | * * * Skynet portals have file size limits (in megabytes), if the media is larger than this limit it will be compressed (using ffmpeg) to prevent upload errors 88 | * * --namebase_access_key 89 | * * * Access key, secret key, and domain name are needed if updating the skylink in namebase 90 | * * --namebase_domain 91 | * * * Access key, secret key, and domain name are needed if updating the skylink in namebase 92 | * * --skynet_instance 93 | * * * If a skylink instance is passed, the skynet links with resolve to this instance i.e. https://skynethub.io/ 94 | 95 | 96 | Usage 97 | ----------- 98 | 99 | Presteps: 100 | a. Have a jellyfin/emby server available with media on it. 101 | b. Have an account with download permissions on that server. (Don't have to be an admin) 102 | 103 | 1. Clone the repo 104 | 105 | 2. Create virtualenv & activate it 106 | 107 | .. code-block:: bash 108 | 109 | python3 -m venv .venv 110 | source .venv/bin/activate 111 | 112 | 3. Install dependencies 113 | 114 | .. code-block:: bash 115 | 116 | pip install -r requirements.txt -r test_requirements.txt 117 | 118 | 4. python uploader.py --all 119 | 120 | 4a. The script may ask you for command line input 121 | 122 | Examples 123 | ------------- 124 | * Upload movies that were added to emby/jellyfin today (you could add this as a cron job to continually share to skynet) 125 | 126 | .. code-block:: bash 127 | 128 | python uploader.py --datecreated `date --iso-8601` --mediatype "Movie" 129 | read config media server 130 | ./cfg/mediaserver-config.json read successfully 131 | Configuring media server connection... 132 | Admin user Password needed to continue: 133 | Downloading item: # 5 - Big Buck Bunny 134 | Uploading file to skynet: big_buck_bunny_480p_surround-fix.avi 135 | Media is now available on skynet: sia://AAApJJPnci_CzFnddB076HGu1_C64T6bfoiQqvsiVB5XeQ 136 | 137 | * Upload all TV episodes 138 | 139 | .. code-block:: bash 140 | 141 | python uploader.py --all --mediatype "Episode" 142 | 143 | * Upload all Movies to your namebase domain 144 | 145 | .. code-block:: bash 146 | 147 | python uploader.py --all --mediatype "Movie" --rss_title "My Media" --skynet_file_size_limit 1000 --namebase_access_key xxx --namebase_secret_key xxxx --namebase_domain MoviesRUs --skynet_instance https://siasky.net/ 148 | 149 | * Example RSS Feed 150 | 151 | .. code-block:: xml 152 | 153 | 154 | 155 | 7371fbee 156 | Open source Movie Feed 157 | 2020-03-16T02:51:23.343056+00:00 158 | 159 | 160 | t-900 161 | 162 | python-feedgen 163 | 164 | 5 165 | Big Buck Bunny 166 | 2020-03-16T02:51:23.343951+00:00 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | Integration Ideas 176 | ----------------- 177 | 178 | Share your emby movies with friends and family without granting them access to emby. Just send them a link to Feeder on F-droid and the link to the rss feed. `https://siasky.net/hns/` 179 | 180 | Thanks to 181 | ----------------- 182 | ![CryptoHO.ST](https://cryptoho.st/images/cryptohost_logo_sized.3.png) 183 | 184 | 185 | Fast and reliable VPS store. Accepting Monero, Bitcoin, Lightning Network, Litecoin and Dash. 186 | 187 | https://cryptoho.st/ 188 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | markdown: GFM 2 | -------------------------------------------------------------------------------- /file_operations.py: -------------------------------------------------------------------------------- 1 | from mediaServer.item import Item 2 | import siaskynet as skynet 3 | import os 4 | import logging 5 | import ffmpeg 6 | 7 | _log = logging.getLogger(__name__) 8 | 9 | 10 | def clean_up(file): 11 | os.remove(file) 12 | 13 | 14 | def download_then_upload(mediaserver, item_to_upload: Item, compression_size) -> Item: 15 | print(f"Downloading item: # {item_to_upload.id} - {item_to_upload.name}") 16 | try: 17 | downloaded_file, content_type = mediaserver.download_item(item_to_upload) 18 | except Exception as inst: 19 | _log.critical(inst) 20 | 21 | 22 | if compression_size: 23 | # item_to_upload.size is in bytes 24 | # compression_size is in megabytes 25 | if item_to_upload.size > compression_size * 1000000: 26 | file_to_upload = downloaded_file[:-4] + ".webm" 27 | print(f'{downloaded_file} file larger than skynet file size limit: Compressing before upload') 28 | print('THIS WILL TAKE A LONG TIME') 29 | # determine target bitrate (bits / sec) 30 | bitrate = (compression_size * 1000000 * 8) / item_to_upload.duration_in_sec 31 | ffmpeg.input(downloaded_file).\ 32 | output(file_to_upload, 33 | **{'c:v': 'libaom-av1'}, # file size small, slower encoding 34 | # **{'c:v': 'libvpx-vp9'}, # file size too large, quicker encoding 35 | **{'b:v': bitrate}, 36 | **{'strict': '-2'}, 37 | **{'cpu-used': '8'})\ 38 | .run() 39 | file_to_upload_size = os.path.getsize(file_to_upload) 40 | else: 41 | file_to_upload = downloaded_file 42 | file_to_upload_size = item_to_upload.size 43 | bitrate = item_to_upload.totalbitrate / item_to_upload.duration_in_sec 44 | else: 45 | file_to_upload = downloaded_file 46 | file_to_upload_size = item_to_upload.size 47 | bitrate = item_to_upload.totalbitrate / item_to_upload.duration_in_sec 48 | 49 | 50 | # TODO upload subtitle files for the media 51 | # TODO upload artwork for media i.e. album cover, movie poster, etc 52 | print(f"Uploading file to skynet: {file_to_upload}") 53 | try: 54 | skylink = skynet.upload_file(file_to_upload) 55 | # skylink = 'sia://AAB65241yS2qpoDIcNrjM4cxc9KY3rGQs_uPmN_M3b-WYw' 56 | # skylink = 'https://siasky.net/AAB65241yS2qpoDIcNrjM4cxc9KY3rGQs_uPmN_M3b-WYw' 57 | print(f"Media is now available on skynet: {skylink}") 58 | except Exception as inst: 59 | _log.critical(inst) 60 | 61 | # upload primary image for item 62 | try: 63 | image_file = mediaserver.download_item_image(item_to_upload, width=176, height=264) 64 | print(image_file) 65 | except Exception as inst: 66 | _log.critical(inst) 67 | 68 | try: 69 | skylink_image = skynet.upload_file(image_file) 70 | # skylink_image = 'sia://vAFrKIUSUBoGPb0wQZ1QK9INQDnx2M4yiPj-oc_dhTFlqQ' 71 | # skylink_image = 'https://siasky.net/vAFrKIUSUBoGPb0wQZ1QK9INQDnx2M4yiPj-oc_dhTFlqQ' 72 | print(f"Primary Image is now available on skynet: {skylink_image}") 73 | except Exception as inst: 74 | _log.critical(inst) 75 | 76 | try: 77 | # clean up the compressed file 78 | clean_up(file_to_upload) 79 | if file_to_upload != downloaded_file: 80 | # clean up the uncompressed file if need be 81 | clean_up(downloaded_file) 82 | # clean up the primary image 83 | clean_up(image_file) 84 | except Exception as inst: 85 | _log.critical(inst) 86 | 87 | try: 88 | setattr(item_to_upload, "skylink", skylink) 89 | setattr(item_to_upload, "size", file_to_upload_size) 90 | setattr(item_to_upload, "mime_type", content_type) 91 | setattr(item_to_upload, "bitrate", bitrate) 92 | setattr(item_to_upload, "skylink_image", skylink_image) 93 | return item_to_upload 94 | except Exception as inst: 95 | _log.critical(inst) 96 | 97 | 98 | -------------------------------------------------------------------------------- /hns_operations.py: -------------------------------------------------------------------------------- 1 | from namebase_exchange.exchange import * 2 | 3 | def update_namebase_dns(api_key: str, secret_key: str, domain: str, sialink : str) -> bool: 4 | updated_records = [] 5 | exchange = Exchange(api_key, secret_key) 6 | dns_settings = exchange.get_dns_settings(domain) 7 | dns_records = dns_settings['records'] 8 | ns_set = False 9 | txt_set = False 10 | for record in dns_records: 11 | # verify a nameserver is already specified 12 | if record['type'] == 'NS' and record['value'] != '': 13 | ns_set = True 14 | # modify the existing TXT record if there is one 15 | if record['type'] == 'TXT': 16 | txt_set = True 17 | record['host'] = '' 18 | record['value'] = sialink 19 | updated_records.append(record) 20 | 21 | if ns_set == False: 22 | # if there is no nameserver add one 23 | # name server ip address from blog: https://www.namebase.io/blog/setting-dns-records/ 24 | updated_records.append({'type':'NS','host':'ns1','value':'44.231.6.183','ttl':0}) 25 | if txt_set == False: 26 | updated_records.append({'type':'TXT','host':'', 27 | 'value':'AAApJJPnci_CzFnddB076HGu1_C64T6bfoiQqvsiVB5XeQ','ttl':0}) 28 | 29 | dns_update_success = exchange.update_dns_settings(domain=domain, 30 | records=updated_records) 31 | try: 32 | dns_update_success = dns_update_success['success'] 33 | except Exception as e: 34 | print(dns_update_success) 35 | raise e 36 | if dns_update_success == False: 37 | print('Namebase DNS record update failed') 38 | else: 39 | print('Namebase DNS record update successful') 40 | 41 | return dns_update_success 42 | 43 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | git+https://github.com/t-900-a/jellyfin-python 2 | git+https://github.com/t-900-a/namebase-exchange-python/ 3 | siaskynet 4 | feedgen 5 | ffmpeg-python 6 | -------------------------------------------------------------------------------- /rss_operations.py: -------------------------------------------------------------------------------- 1 | from feedgen.feed import FeedGenerator 2 | import siaskynet as skynet 3 | 4 | def rewrite_sialink(skynet_instance, sialink): 5 | if skynet_instance is not None and sialink[:6] == 'sia://': 6 | sialink = sialink[6:] 7 | sialink = skynet_instance + sialink 8 | return sialink 9 | else: 10 | return sialink 11 | 12 | def write_rss(medias_with_sialinks, id, title, link, description, contributor, subtitle, skynet_instance) -> str: 13 | # TODO add so that if a media item exists multiple times they are included in the same group 14 | # i.e. 480p, 720p, 1080p, 4k 15 | # TODO include actors as contributors to a media item 16 | try: 17 | fg = FeedGenerator() 18 | fg.load_extension('media', atom=True, rss=True) 19 | fg.id(id) 20 | fg.title(title) 21 | fg.link(href=rewrite_sialink(skynet_instance, link)) 22 | fg.description(description) 23 | fg.contributor(name=contributor) 24 | fg.subtitle(subtitle) 25 | for media in medias_with_sialinks: 26 | fe = fg.add_entry() 27 | fe.id(media.imdb_id) 28 | fe.title(media.name) 29 | fe.summary(media.description) 30 | fe.link(href=rewrite_sialink(skynet_instance, media.skylink)) 31 | # TODO add critic rating to xml tags 32 | fe.media.content({'url': rewrite_sialink(skynet_instance, media.skylink), 33 | 'fileSize': str(media.size), 34 | 'type': media.mime_type, 35 | 'medium': media.media_type, 36 | # 'isDefault':'', 37 | 'expression': 'full', 38 | 'bitrate': str(media.totalbitrate), 39 | 'framerate': str(media.framerate), 40 | 'samplingrate': str(media.samplingrate), 41 | 'channels': str(media.channels), 42 | 'duration': str(media.duration_in_sec), 43 | 'height': str(media.height), 44 | 'width': str(media.width), 45 | 'lang': media.lang}) 46 | 47 | fe.media.thumbnail({'url': rewrite_sialink(skynet_instance, media.skylink_image), 48 | 'height': '264', 49 | 'width': '176'}) 50 | # build summary 51 | # TODO figure out embedding an svg for the imdb rotten tomatoes logo and the associated rating 52 | # imdb_rating = '7.8' 53 | # imdb_badge = requests.get( 54 | # f"https://img.shields.io/static/v1?" 55 | # f"label=Imdb&message={imdb_rating}&color=gold" 56 | # f"&logo=imdb&link=https://www.imdb.com/title/{media.imdb_id}/").text 57 | # tomatoe_score = '74' 58 | # rotten_tomatoes_base64_png = 'data:image/png;base64,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' 59 | # rotten_tomatoes_badge = requests.get( 60 | # f"https://img.shields.io/static/v1?" 61 | # f"label=Rotten%20Tomatoes&message={tomatoe_score}%" 62 | # f"&color=red&logo={rotten_tomatoes_base64_png}" 63 | # ).text 64 | # description_with_ratings = media.description + '\n' + ']]>' 65 | # fe.summary(description_with_ratings) 66 | except Exception as e: 67 | print(e) 68 | pass 69 | 70 | atomfeed = fg.atom_str(pretty=True) # Get the ATOM feed as string 71 | fg.atom_file('atom.xml') # Write the ATOM feed to a file 72 | # upload file to skynet 73 | if skynet_instance is not None: 74 | skylink = skynet.upload_file('atom.xml') 75 | print(f'RSS feed is now available on skynet: {skylink}') 76 | return skylink 77 | else: 78 | return '' 79 | -------------------------------------------------------------------------------- /skynet_uploader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t-900-a/emby-skynet-uploader/cfaa4a6208ef75337dcfa2d9b163ccf79250af53/skynet_uploader.gif -------------------------------------------------------------------------------- /uploader.py: -------------------------------------------------------------------------------- 1 | from configurator.mediaserver import mediaServer_config 2 | from mediaServer.server import MediaServer 3 | from mediaServer.item import Item 4 | import logging 5 | import argparse 6 | from datetime import * 7 | import hashlib 8 | from file_operations import download_then_upload 9 | from rss_operations import write_rss 10 | from hns_operations import update_namebase_dns 11 | 12 | _log = logging.getLogger(__name__) 13 | 14 | 15 | parser = argparse.ArgumentParser(description='Process mediaserver files and upload to skynet') 16 | parser.add_argument('--datecreated', dest='date_created', default=None, type=str, 17 | help=f"iso-8601 date format i.e. {date.today()}" 18 | " all media that was created on or after that date will be uploaded to skynet") 19 | parser.add_argument('--itemid', dest='item_id_to_upload',default=None, type=int, help="the itemid from emby/jellyfin" 20 | "that you would like to upload to skynet") 21 | parser.add_argument('--all', 22 | default=False, dest='import_all', action='store_const', const=True, 23 | help="include this argument if you would like to upload all media found on the server to skynet") 24 | 25 | parser.add_argument('--mediatype', default="Movie,Episode", 26 | dest='media_type_to_upload', type=str, 27 | help="specify the type of media from the media server you would like to upload" 28 | " i.e. Movie" 29 | " default: Movie,Episode") 30 | parser.add_argument('--mediaserverconfig', dest='media_server_config', default='mediaserver-config.json', type=str, 31 | help="config file for the emby/jellyfin server connection" 32 | " file will be generated if none exists") 33 | 34 | parser.add_argument('--rss_id', dest='rss_id', default=hashlib.md5(bytes(datetime.now().__str__(), "ascii")).hexdigest()[:8], type=str, 35 | help="Id for your channel" 36 | " i.e. 4c6ca4f7") 37 | 38 | parser.add_argument('--rss_title', dest='rss_title', default=None, type=str, 39 | help="The name of your rss channel, if none is specified an rss feed will not be generated" 40 | " i.e. anon's Media") 41 | 42 | parser.add_argument('--rss_link', dest='rss_link', default='sia://fALzGYpbWAhwBu3Qs5z0MUbTbBUQ117rnERnqlRmaR-HiA', 43 | type=str, help="Include a link if you would like your site to be referenced" 44 | "i.e. https://www.mysharedmedia.com/jellyfin/") 45 | 46 | parser.add_argument('--rss_description', dest='rss_description', default='Information wants to be free', 47 | type=str, help="Choose the description of the feed") 48 | 49 | parser.add_argument('--rss_contributor', dest='rss_contributor', default='anon', 50 | type=str, help="Specify a contributor") 51 | 52 | parser.add_argument('--rss_subtitle', dest='rss_subtitle', default='', 53 | type=str, help="Addition comment for your site if you want it" 54 | "i.e. For more content, please donate _cryptocurrency_symbol to _cryptocurrency_address") 55 | 56 | parser.add_argument('--skynet_file_size_limit', dest='compression_size', default=None, type=int, 57 | help="Skynet portals have file size limits (in megabytes), if the media is larger" 58 | "than this limit it will be compressed to prevent upload errors") 59 | 60 | parser.add_argument('--namebase_access_key', dest='namebase_access_key', default=None, type=str, 61 | help="Access key, secret key, and domain name are needed if updating the skylink in namebase") 62 | 63 | parser.add_argument('--namebase_secret_key', dest='namebase_secret_key', default=None, type=str, 64 | help="Access key, secret key, and domain name are needed if updating the skylink in namebase") 65 | 66 | parser.add_argument('--namebase_domain', dest='namebase_domain', default=None, type=str, 67 | help="Access key, secret key, and domain name are needed if updating the skylink in namebase") 68 | 69 | parser.add_argument('--skynet_instance', dest='skynet_instance', default=None, type=str, 70 | help="If a skylink instance is passed, the skynet links with resolve to this instance" 71 | "i.e. https://skynethub.io/") 72 | 73 | args = parser.parse_args() 74 | # TODO Add support for tv shows and anime 75 | # you'll need to add a function for direct upload within file_operations.py 76 | # i.e. if your the server admin, you want to use the full path as there is no need to download from your server 77 | # i.e. if your a user, you want to download the file from the remote server, then upload to skynet 78 | def main(): 79 | medias_with_sialinks = [] 80 | config = mediaServer_config(cfg_file=args.media_server_config) 81 | try: 82 | mediaserver = MediaServer(config) 83 | except Exception as inst: 84 | _log.critical(inst) 85 | 86 | if args.import_all == True: 87 | medias = mediaserver.get_items(include_item_types=args.media_type_to_upload, recursive="true", fields="Path%2C%20MediaStreams%2C%20ProviderIds%2C%20Overview") 88 | for item_to_upload in medias: 89 | item_with_sialink = download_then_upload(mediaserver, item_to_upload, args.compression_size) 90 | medias_with_sialinks.append(item_with_sialink) 91 | 92 | if args.item_id_to_upload is not None: 93 | item_to_upload = mediaserver.get_item(int(args.item_id_to_upload), fields="Path%2C%20MediaStreams%2C%20ProviderIds%2C%20Overview") 94 | item_with_sialink = download_then_upload(mediaserver, item_to_upload, args.compression_size) 95 | medias_with_sialinks.append(item_with_sialink) 96 | 97 | if args.date_created is not None: 98 | medias = mediaserver.get_items(include_item_types=args.media_type_to_upload, sort_by="DateCreated", 99 | sort_order="Descending", recursive="true", fields="DateCreated%2C%20Path%2C%20MediaStreams%2C%20ProviderIds%2C%20Overview") 100 | 101 | for item_to_upload in medias: 102 | if (datetime.strptime(item_to_upload.date_created[:10], '%Y-%m-%d') >= datetime.strptime(args.date_created, '%Y-%m-%d')): 103 | item_with_sialink = download_then_upload(mediaserver, item_to_upload, args.compression_size) 104 | medias_with_sialinks.append(item_with_sialink) 105 | else: 106 | break 107 | 108 | if args.namebase_access_key and args.namebase_secret_key and args.namebase_domain: 109 | rss_sialink = write_rss(medias_with_sialinks, args.rss_id, args.rss_title, args.rss_link, 110 | args.rss_description, args.rss_contributor, args.rss_subtitle, skynet_instance=args.skynet_instance) 111 | dns_update_success = update_namebase_dns(args.namebase_access_key, 112 | args.namebase_secret_key, 113 | args.namebase_domain, 114 | rss_sialink) 115 | if dns_update_success: 116 | print(f'The rss feed should soon be available at any skynet instance, ' 117 | f'example: {args.skynet_instance}hns/{args.namebase_domain}/') 118 | else: 119 | write_rss(medias_with_sialinks, args.rss_id, args.rss_title, args.rss_link, 120 | args.rss_description, args.rss_contributor, args.rss_subtitle, skynet_instance=args.skynet_instance) 121 | 122 | 123 | exit(0) 124 | 125 | 126 | 127 | if __name__ =="__main__": 128 | main() 129 | --------------------------------------------------------------------------------