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 <fijxu@nadeko.net>
This commit is contained in:
unrealtournament
2026-10-31 21:34:46 +01:00
committed by GitHub
parent 9f0b4c89f7
commit 1dc3335ce6
4 changed files with 52 additions and 18 deletions

View File

@@ -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",

View File

@@ -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)

View File

@@ -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)

View File

@@ -53,11 +53,11 @@
<legend><%= I18n.translate(locale, "Export") %></legend>
<div class="pure-control-group">
<a href="/subscription_manager?action_takeout=1"><%= I18n.translate(locale, "Export subscriptions as OPML") %></a>
<a href="/subscription_manager?action_takeout=1&format=opml"><%= I18n.translate(locale, "Export subscriptions as OPML (for FreeTube)") %></a>
</div>
<div class="pure-control-group">
<a href="/subscription_manager?action_takeout=1&format=newpipe"><%= I18n.translate(locale, "Export subscriptions as OPML (for NewPipe & FreeTube)") %></a>
<a href="/subscription_manager?action_takeout=1&format=newpipe_json"><%= I18n.translate(locale, "Export subscriptions as JSON (for NewPipe)") %></a>
</div>
<div class="pure-control-group">