Initial commit

This commit is contained in:
dtb
2025-09-11 21:27:18 -06:00
commit ed900f1e4a
14 changed files with 1201 additions and 0 deletions

29
examples/README.ytfeed Normal file
View File

@@ -0,0 +1,29 @@
```
____ __ __ __
/_/ / /_ /_/ /_/ _/ /\
__/ / / /_ /_ /_/ /__\ public domain 2020-2025
```
ytfeed(6) is a small and simple YouTube client made using menu(1). ytfeed(6)
exists as a demonstration of menu(1), menu(1) was written as a component of
ytfeed(6), and they were both written for my own personal use.
There are many components of ytfeed that work independently of it.
ytfeed.aggregate(1) is a tool to merge many catenated YouTube RSS feeds into
one XML-compliant document. The invocation
`cat feed.xml feed2.xml | ytfeedmerge`, for example, assuming the given XML
files are YouTube RSS feeds, will output a single YouTube RSS feed containing
the entries from both feeds.
ytfeed.dl(1) is a tool to download a YouTube RSS feed using curl(1) or wget(1)
and store it in the location used by ytfeed(6).
The Python programs are especially short, exclusively use Python's standard
library, and are written to (hopefully) work on a reasonably recent Python 3
interpreter. Unfortunately Python will probably shit itself anyway when you try
to run them. For what it's worth, they're Python programs because most Linux
distributions come with Python 3 and it's one of few languages with XML parsing
as part of the standard library - I figured it'd be less hassle than vendoring
a dependency or (woe be unto those who think it) relying on whatever awful
package managers the user has installed.

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns:yt="http://www.youtube.com/xml/schemas/2015" xmlns:media="http://search.yahoo.com/mrss/" xmlns="http://www.w3.org/2005/Atom">
<link rel="self" href="http://www.youtube.com/feeds/videos.xml?channel_id=UC4QobU6STFB0P71PMvOGN5A"/>
<id>yt:channel:4QobU6STFB0P71PMvOGN5A</id>
<yt:channelId>4QobU6STFB0P71PMvOGN5A</yt:channelId>
<title>jawed</title>
<link rel="alternate" href="https://www.youtube.com/channel/UC4QobU6STFB0P71PMvOGN5A"/>
<author>
<name>jawed</name>
<uri>https://www.youtube.com/channel/UC4QobU6STFB0P71PMvOGN5A</uri>
</author>
<published>2005-04-24T03:20:54+00:00</published>
<entry>
<id>yt:video:jNQXAC9IVRw</id>
<yt:videoId>jNQXAC9IVRw</yt:videoId>
<yt:channelId>UC4QobU6STFB0P71PMvOGN5A</yt:channelId>
<title>Me at the zoo</title>
<link rel="alternate" href="https://www.youtube.com/watch?v=jNQXAC9IVRw"/>
<author>
<name>jawed</name>
<uri>https://www.youtube.com/channel/UC4QobU6STFB0P71PMvOGN5A</uri>
</author>
<published>2005-04-24T03:31:52+00:00</published>
<updated>2025-08-22T10:26:34+00:00</updated>
<media:group>
<media:title>Me at the zoo</media:title>
<media:content url="https://www.youtube.com/v/jNQXAC9IVRw?version=3" type="application/x-shockwave-flash" width="640" height="390"/>
<media:thumbnail url="https://i3.ytimg.com/vi/jNQXAC9IVRw/hqdefault.jpg" width="480" height="360"/>
<media:description>Microplastics are accumulating in human brains at an alarming rate
https://www.youtube.com/watch?v=0PT5c1z3LL8
“Nanoplastics and Human Health” with Matthew J Campen, PhD, MSPH
https://www.youtube.com/watch?v=RRBN_4L09Mg
00:00 Intro
00:05 The cool thing
00:17 End</media:description>
<media:community>
<media:starRating count="18205539" average="5.00" min="1" max="5"/>
<media:statistics views="371276149"/>
</media:community>
</media:group>
</entry>
</feed>

47
examples/ytfeed Executable file
View File

@@ -0,0 +1,47 @@
#!/bin/sh
# AGPLv3
test -n "$ytfeed_feeds_dir" || ytfeed_feeds_dir=feeds/
export ytfeed_feeds_dir
printf "\
____ __ __ __
/_/ / /_ /_/ /_/ _/ /\\ ytfeed 2.0.0-pre
__/ / / /_ /_ /_/ /__\\ dtb 2020-2025
"
../menu <<EOF
Subscribe to new feed.
printf 'Please enter the channel ID to which to be subscribed: '
</dev/tty \
head -n 1 \
| xargs ytfeed.dl "$ytfeed_feeds_dir"
Browse feeds.
ytfeed.browse-feeds "$ytfeed_feeds_dir" \
| ../menu
Browse all feeds.
cat "$ytfeed_feeds_dir"/*.xml \
| ytfeed.aggregate \
| ytfeed.browse-feed \
| ../menu
Refresh feeds.
for f in "$ytfeed_feeds_dir"/*.xml
do printf '%s\\n' "\$f" \
| sed -e 's,^.*/,,' \
-e 's,\\.xml\$,,' \
| xargs ytfeed.dl "$ytfeed_feeds_dir"
done
Show configuration
printf 'ytfeed_feeds_dir: %s\n' "$ytfeed_feeds_dir" >&2
EOF

26
examples/ytfeed.aggregate Executable file
View File

@@ -0,0 +1,26 @@
#!/usr/bin/env python3
# 2025 dtb. public domain
import re, sys, xml.etree.ElementTree as ET
ET.register_namespace("", "http://www.w3.org/2005/Atom")
ET.register_namespace("media", "http://search.yahoo.com/mrss/")
ET.register_namespace("yt", "http://www.youtube.com/xml/schemas/2015")
def sortby(entry):
return entry.findall("{http://www.w3.org/2005/Atom}published")[0].text
macrofeed = ET.Element('feed')
macrofeed.extend(
sorted([
entry
# Split the input by file.
for feed in re.split(r'<\?xml.*?\?>', sys.stdin.read())[1:]
# Get the entries out of each file.
for entry
in ET.fromstring(feed)
.findall("{http://www.w3.org/2005/Atom}entry")
],
# Gets the publication date and sorts the entries from old to new.
key = lambda entry
: entry.findall("{http://www.w3.org/2005/Atom}published")[0].text,
reverse = False # toggle this to switch the order
)
)
print(ET.tostring(macrofeed, encoding="unicode"))

37
examples/ytfeed.browse-entry Executable file
View File

@@ -0,0 +1,37 @@
#!/usr/bin/env python3
# 2025 dtb. public domain
import sys, xml.etree.ElementTree as ET
def f(fmt, e, t):
r = e.find(t, {
"": "http://www.w3.org/2005/Atom",
"media": "http://search.yahoo.com/mrss/",
"yt": "http://www.youtube.com/xml/schemas/2015"
})
return fmt % r.text if r is not None else ""
tree = ET.parse(sys.stdin)
root = tree.getroot()
ytid = f("%s", root, "yt:videoId")
print(
f("[%s] ", root, "published")
+ f("%s - ", root, "author/name")
+ f("%s", root, "title")
+ "\n\n"
+ f("%s", root, "media:group/media:description"),
end = "\n\n",
file = sys.stderr
)
print(
"#!/usr/bin/env menu" + "\n\n"
+ "Download feed." + "\n\n"
+ "\t" + "yt-dlp %s" % ytid + "\n\n"
+ "Print feed URL." + "\n\n"
+ "\t" + "printf 'https://youtube.com/watch?v=%%s\\n' '%s'" % ytid
+ "\n\n"
+ "Open feed URL in mpv." + "\n\n"
# + "\t" + "mpv 'https://youtube.com/watch?v=%s'" % ytid + "\n\n"
+ "\t" + "mpv" + " \\\n"
+ "\t\t" + "--gpu-sw --profile=fast" + " \\\n"
+ "\t\t" + "--script-opts=ytdl_hook-all_formats=yes" + " \\\n"
+ "\t\t" + "'https://youtube.com/watch?v=%s'" % ytid,
end = "\n\n"
)

28
examples/ytfeed.browse-feed Executable file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env python3
# 2025 dtb. public domain
import sys, xml.etree.ElementTree as ET
tree = ET.parse(sys.stdin)
root = tree.getroot()
def f(fmt, e, t, fn=None):
r = e.find(t, {
"": "http://www.w3.org/2005/Atom",
"media": "http://search.yahoo.com/mrss/",
"yt": "http://www.youtube.com/xml/schemas/2015}videoId"
})
r = r.text if r is not None else ""
if fn is not None: r = fn(r)
return fmt % r
print("#!/usr/bin/env menu", end = "\n\n") # lead-in
for entry in root.findall("{http://www.w3.org/2005/Atom}entry"):
print(
# Text
f("[%s] ", entry, "published", fn = lambda s : s.split("T")[0])
+ f("%-22s - ", entry, "author/name")
+ f("%s", entry, "title") + "\n\n"
# Command
+ "\t" + "ytfeed.browse-entry <<EOF | ../menu" + "\n"
+ "\t" + ET.tostring(entry, encoding="unicode")
.replace("\n", "\n\t").rstrip() + "\n"
+ "\tEOF",
end = "\n\n"
)

22
examples/ytfeed.browse-feeds Executable file
View File

@@ -0,0 +1,22 @@
#!/usr/bin/env python3
# 2025 dtb. public domain
import os, sys, xml.etree.ElementTree as ET
if len(sys.argv) != 2:
print("Usage: %s directory\n" % sys.argv[0], file = sys.stderr)
sys.exit(64) # sysexits(3) EX_USAGE
channels = []
directory = sys.argv[1]
for f in os.listdir(directory):
file_name = os.path.join(directory, f)
if os.path.isfile(file_name) and f[-4:] == ".xml":
tree = ET.parse(file_name)
root = tree.getroot()
try:
channels += [
"%s\n\n" % root.find('{http://www.w3.org/2005/Atom}title').text
+ "\t<'%s' ytfeed.browse-feed | ../menu\n\n" % file_name
]
except: pass
print("#!/usr/bin/env menu", end = "\n\n")
for s in sorted(channels, key=str.lower): # Sorts alphabetically (caseless).
print(s, end = "")

21
examples/ytfeed.dl Executable file
View File

@@ -0,0 +1,21 @@
#!/bin/sh
# 2025 dtb. public domain
directory="$1"
alias have='command -v >/dev/null 2>&1'
xml_url_prefix='https://www.youtube.com/feeds/videos.xml?channel_id='
if test -z "$2"; then
printf 'Usage: %s directory channel_id...\n' "$0" >&2
exit 64 # sysexits(3) EX_USAGE
fi
while test -n "$2"; do
if have curl; then curl=curl
elif have wget; then curl='wget -O -'
else curl=false
fi
filename="$(printf '%s/%s.xml\n' "$directory" "$2")"
if ! $curl "$xml_url_prefix""$2" >"$filename"
then rm -f "$filename"
else printf '%s\n' "$filename"
fi
shift
done