From 1dc3335ce6c64b5d6f7b0ab59a313b19f99b7b53 Mon Sep 17 00:00:00 2001 From: unrealtournament <111871385+unrealtournament@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:34:46 +0100 Subject: [PATCH] Add subscription JSON export for NewPipe (#5826) The current option to export subscriptions for NewPipe (`Export subscriptions as OPML (for NewPipe & FreeTube)`) isn't accepted by NewPipe anymore. As far as I can tell, NewPipe only accepts importing from its own SQLite database, from Google Account CSV export and from its own JSON export format. This is mentioned in https://github.com/iv-org/invidious/issues/5438. This PR adds support to export subscriptions in the NewPipe compatible format. I'm looking for input on a few points: - I'm not a huge fan of hardcoding the NewPipe version in the export, but it is required by NewPipe to recognize it as a valid file. If someone has a better idea I'm all ears but I think it's the only way. In any case, NewPipe can import formats in a backwards compatible way in the future so it shouldn't be a huge deal. - I haven't checked if the OPML export option actually works for FreeTube. If it doesn't, it should probably be removed altogether, for now I just removed the `(for NewPipe)` part. - The translations would need to be updated, but I don't know how that is handled in this project so I need some help there. The new format looks like this: ``` { "subscriptions": [ { "service_id": 0, "url": "https://www.youtube.com/channel/UCHnyfMqiRRG1u-2MsSQLbXA", "name": "Veritasium" }, { "service_id": 0, "url": "https://www.youtube.com/channel/UC6nSFpj9HTCZ5t-N3Rm3-HA", "name": "Vsauce" } ], "app_version": "0.29.0", "app_version_int": 1014 } ``` * add subscription json export for NewPipe * remove old newpipe export format * make newpipe export values constant * remove duplicate export option, add i18n keys * Convert to_newpipe to a serializable struct * Use same naming format as NewPipe for the exported subscriptions --------- Co-authored-by: Fijxu --- locales/en-US.json | 4 +-- src/invidious/routes/subscriptions.cr | 26 ++++++++-------- src/invidious/user/exports.cr | 36 +++++++++++++++++++++++ src/invidious/views/user/data_control.ecr | 4 +-- 4 files changed, 52 insertions(+), 18 deletions(-) diff --git a/locales/en-US.json b/locales/en-US.json index 8da3ccda7..2d2217917 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -51,8 +51,8 @@ "Import NewPipe subscriptions (.json)": "Import NewPipe subscriptions (.json)", "Import NewPipe data (.zip)": "Import NewPipe data (.zip)", "Export": "Export", - "Export subscriptions as OPML": "Export subscriptions as OPML", - "Export subscriptions as OPML (for NewPipe & FreeTube)": "Export subscriptions as OPML (for NewPipe & FreeTube)", + "Export subscriptions as OPML (for FreeTube)": "Export subscriptions as OPML (for FreeTube)", + "Export subscriptions as JSON (for NewPipe)": "Export subscriptions as JSON (for NewPipe)", "Export data as JSON": "Export Invidious data as JSON", "Delete account?": "Delete account?", "History": "History", diff --git a/src/invidious/routes/subscriptions.cr b/src/invidious/routes/subscriptions.cr index 6c7422116..e07b62c91 100644 --- a/src/invidious/routes/subscriptions.cr +++ b/src/invidious/routes/subscriptions.cr @@ -80,30 +80,28 @@ module Invidious::Routes::Subscriptions subscriptions.sort_by!(&.author.downcase) if action_takeout - if format == "json" - env.response.content_type = "application/json" - env.response.headers["content-disposition"] = "attachment" + env.response.content_type = "application/json" + # Exports the NewPipe with the same naming format NewPipe + # uses for their exports + # Exmaple: `newpipe_subscriptions_202610312013` + export_time = Time.local.to_s("%Y%m%d%H%M") + env.response.headers["Content-Disposition"] = "attachment; filename=\"newpipe_subscriptions_#{export_time}\"" + + if format == "json" return Invidious::User::Export.to_invidious(user) + elsif format == "newpipe_json" + return Invidious::User::Export.to_newpipe(subscriptions) else env.response.content_type = "application/xml" - env.response.headers["content-disposition"] = "attachment" export = XML.build do |xml| xml.element("opml", version: "1.1") do xml.element("body") do - if format == "newpipe" - title = "YouTube Subscriptions" - else - title = "Invidious Subscriptions" - end + title = "Invidious Subscriptions" xml.element("outline", text: title, title: title) do subscriptions.each do |channel| - if format == "newpipe" - xml_url = "https://www.youtube.com/feeds/videos.xml?channel_id=#{channel.id}" - else - xml_url = "#{HOST_URL}/feed/channel/#{channel.id}" - end + xml_url = "#{HOST_URL}/feed/channel/#{channel.id}" xml.element("outline", text: channel.author, title: channel.author, "type": "rss", xmlUrl: xml_url) diff --git a/src/invidious/user/exports.cr b/src/invidious/user/exports.cr index 9ce823ae9..adc604825 100644 --- a/src/invidious/user/exports.cr +++ b/src/invidious/user/exports.cr @@ -2,6 +2,42 @@ struct Invidious::User module Export extend self + struct NewPipe + include JSON::Serializable + + # Hardcoded NewPipe values that are needed + # so NewPipe can import the JSON. + private APP_VERSION = "0.29.0" + private APP_VERSION_INT = 1014 + private YT_SERVICE_ID = 0 + + getter subscriptions : Array(Subscription) + + struct Subscription + include JSON::Serializable + + getter service_id : Int32 = YT_SERVICE_ID + getter url : String + getter name : String + + def initialize(channel : InvidiousChannel) + @url = "https://www.youtube.com/channel/" + channel.id + @name = channel.author + end + end + + getter app_version : String = APP_VERSION + getter app_version_int : Int32 = APP_VERSION_INT + + def initialize(channels : Array(InvidiousChannel)) + @subscriptions = channels.map { |channel| Subscription.new(channel) } + end + end + + def to_newpipe(subscriptions : Array(InvidiousChannel)) + NewPipe.new(subscriptions).to_json + end + def to_invidious(user : User) playlists = Invidious::Database::Playlists.select_like_iv(user.email) diff --git a/src/invidious/views/user/data_control.ecr b/src/invidious/views/user/data_control.ecr index 1ada97122..a7271703f 100644 --- a/src/invidious/views/user/data_control.ecr +++ b/src/invidious/views/user/data_control.ecr @@ -53,11 +53,11 @@ <%= I18n.translate(locale, "Export") %>
- <%= I18n.translate(locale, "Export subscriptions as OPML") %> + <%= I18n.translate(locale, "Export subscriptions as OPML (for FreeTube)") %>
- <%= I18n.translate(locale, "Export subscriptions as OPML (for NewPipe & FreeTube)") %> + <%= I18n.translate(locale, "Export subscriptions as JSON (for NewPipe)") %>