From cd3383e6e3aaf6ad4b014f16bdd9725f203a6924 Mon Sep 17 00:00:00 2001
From: James Taylor <user234683@users.noreply.github.com>
Date: Wed, 1 Sep 2021 19:13:40 -0700
Subject: Add NewPipe subscriptions import and export
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Closes #82

Signed-off-by: Jesús <heckyel@hyperbola.info>
---
 youtube/subscriptions.py                    | 57 ++++++++++++++++++++++++-----
 youtube/templates/subscription_manager.html |  3 +-
 2 files changed, 49 insertions(+), 11 deletions(-)

diff --git a/youtube/subscriptions.py b/youtube/subscriptions.py
index 6ab12c7..7a2bf7d 100644
--- a/youtube/subscriptions.py
+++ b/youtube/subscriptions.py
@@ -684,7 +684,7 @@ def check_specific_channels(channel_ids):
     channel_names.update(channel_id_name_list)
     check_channels_if_necessary(channel_ids)
 
-
+CHANNEL_ID_RE = re.compile(r'UC[-_\w]{22}')
 @yt_app.route('/import_subscriptions', methods=['POST'])
 def import_subscriptions():
 
@@ -702,15 +702,36 @@ def import_subscriptions():
     mime_type = file.mimetype
 
     if mime_type == 'application/json':
-        file = file.read().decode('utf-8')
+        info = file.read().decode('utf-8')
+        if info == '':
+            return '400 Bad Request: File is empty', 400
         try:
-            file = json.loads(file)
+            info = json.loads(info)
         except json.decoder.JSONDecodeError:
             traceback.print_exc()
             return '400 Bad Request: Invalid json file', 400
 
+        channels = []
         try:
-            channels = ((item['snippet']['resourceId']['channelId'], item['snippet']['title']) for item in file)
+            if 'app_version_int' in info:   # NewPipe Format
+                for item in info['subscriptions']:
+                    # Other service, such as SoundCloud
+                    if item.get('service_id', 0) != 0:
+                        continue
+                    channel_url = item['url']
+                    channel_id_match = CHANNEL_ID_RE.search(channel_url)
+                    if channel_id_match:
+                        channel_id = channel_id_match.group(0)
+                    else:
+                        print('WARNING: Could not find channel id in url',
+                              channel_url)
+                        continue
+                    channels.append((channel_id, item['name']))
+            else:   # Old Google Takeout format
+                for item in info:
+                    snippet = item['snippet']
+                    channel_id = snippet['resourceId']['channelId']
+                    channels.append((channel_id, snippet['title']))
         except (KeyError, IndexError):
             traceback.print_exc()
             return '400 Bad Request: Unknown json structure', 400
@@ -738,8 +759,7 @@ def import_subscriptions():
         for row in reader:
             if not row or row[0].lower().strip() == 'channel id':
                 continue
-            elif len(row) > 1 and re.fullmatch(r'UC[-_\w]{22}',
-                                               row[0].strip()):
+            elif len(row) > 1 and CHANNEL_ID_RE.fullmatch(row[0].strip()):
                 channels.append( (row[0], row[-1]) )
             else:
                 print('WARNING: Unknown row format:', row)
@@ -767,7 +787,7 @@ def export_subscriptions():
                     _get_subscribed_channels(cursor)):
                 if muted and not include_muted:
                     continue
-                if request.values['export_format'] == 'json':
+                if request.values['export_format'] == 'json_google_takeout':
                     sub_list.append({
                         'kind': 'youtube#subscription',
                         'snippet': {
@@ -780,21 +800,38 @@ def export_subscriptions():
                             'title': channel_name,
                         },
                     })
+                elif request.values['export_format'] == 'json_newpipe':
+                    sub_list.append({
+                        'service_id': 0,
+                        'url': 'https://www.youtube.com/channel/' + channel_id,
+                        'name': channel_name,
+                    })
                 elif request.values['export_format'] == 'opml':
                     sub_list.append({
                         'channel_name': channel_name,
                         'channel_id': channel_id,
                     })
-    if request.values['export_format'] == 'json':
+    date_time = time.strftime('%Y%m%d%H%M', time.localtime())
+    if request.values['export_format'] == 'json_google_takeout':
         r = flask.Response(json.dumps(sub_list), mimetype='text/json')
-        cd = 'attachment; filename="subscriptions.json"'
+        cd = 'attachment; filename="subscriptions_%s.json"' % date_time
+        r.headers['Content-Disposition'] = cd
+        return r
+    elif request.values['export_format'] == 'json_newpipe':
+        r = flask.Response(json.dumps({
+            'app_version': '0.21.9',
+            'app_version_int': 975,
+            'subscriptions': sub_list,
+        }), mimetype='text/json')
+        file_name = 'newpipe_subscriptions_%s_youtube-local.json' % date_time
+        cd = 'attachment; filename="%s"' % file_name
         r.headers['Content-Disposition'] = cd
         return r
     elif request.values['export_format'] == 'opml':
         r = flask.Response(
             flask.render_template('subscriptions.xml', sub_list=sub_list),
             mimetype='text/xml')
-        cd = 'attachment; filename="subscriptions.xml"'
+        cd = 'attachment; filename="subscriptions_%s.xml"' % date_time
         r.headers['Content-Disposition'] = cd
         return r
     else:
diff --git a/youtube/templates/subscription_manager.html b/youtube/templates/subscription_manager.html
index 5e240b2..cf51a9b 100644
--- a/youtube/templates/subscription_manager.html
+++ b/youtube/templates/subscription_manager.html
@@ -29,7 +29,8 @@
             <h2>Export subscriptions</h2>
             <div class="subscriptions-export-options">
                 <select id="export-type" name="export_format" title="Export format">
-                    <option value="json">JSON</option>
+                    <option value="json_newpipe">JSON (NewPipe)</option>
+                    <option value="json_google_takeout">JSON (Old Google Takeout Format)</option>
                     <option value="opml">OPML (RSS, no tags)</option>
                 </select>
                 <label for="include-muted">Include muted</label>
-- 
cgit v1.2.3