@@ 1,21 @@
+MIT License
+
+Copyright (c) 2023 sungo
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
@@ 1,18 @@
+# yt-thumbs
+
+Let's say you like to download youtube playlists or channels to local storage. Let's say you stick those on a local web server. By default, you're going to get a big list of files which is pretty boring. So, I wanted a thumbnail grid. That's what this does.
+
+It's highly dependent on my preferred way to grab playlists:
+
+`yt-dlp --write-thumbnail --convert-thumbnail jpg --embed-subs --embed-thumbnail --embed-metadata --embed-chapters --write-auto-subs -o '%(upload_date>%Y-%m-%d)s - %(fulltitle)s.%(ext)s' --download-archive archive.txt -f mp4 'https://youtube.com/@[USER]/videos'`
+
+This gets you a directory of mp4 videos and thumbnail files where the difference between the two is just the extension.
+
+# Build
+
+`make`
+
+# Run
+
+`./yt-thumbs -dir=/var/www/[USER]_videos -output=/var/www/[USER]_videos/index.html`
+
@@ 1,136 @@
+package main
+
+/*
+MIT License
+
+Copyright (c) 2023 sungo
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+import (
+ "bytes"
+ "flag"
+ "fmt"
+ "log"
+ "net/url"
+ "os"
+ "sort"
+ "strings"
+ "time"
+)
+
+func main() {
+ args := struct {
+ Dir string
+ Output string
+ DefaultThumb string
+ }{}
+
+ flag.StringVar(&args.Dir, "dir", "", "Dir containing videos and thumbs")
+ flag.StringVar(&args.Output, "output", "", "File in which to write the html output")
+ flag.StringVar(&args.DefaultThumb, "defaultThumb", "NA%20-%20NA.jpg", "Default thumbnail file")
+ flag.Parse()
+
+ if len(args.Dir) == 0 {
+ flag.PrintDefaults()
+ log.Fatal("Please provide -dir")
+ }
+
+ if len(args.Output) == 0 {
+ flag.PrintDefaults()
+ log.Fatal("Please provide -output")
+ }
+
+ entries, err := os.ReadDir(args.Dir)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ var (
+ buf bytes.Buffer
+ col = 0
+ dir = strings.TrimSuffix(args.Dir, "/")
+ files = make([]string, 0)
+ )
+
+ for idx := range entries {
+ entry := entries[idx]
+
+ if entry.IsDir() {
+ continue
+ }
+
+ info, err := entry.Info()
+ if err != nil {
+ log.Printf(err.Error())
+ }
+
+ name := info.Name()
+ if strings.HasSuffix(name, ".mp4") {
+ log.Printf("Found: %s\n", info.Name())
+ files = append(files, info.Name())
+ }
+ }
+
+ if len(files) == 0 {
+ log.Fatal("no files found")
+ }
+
+ sort.Sort(sort.Reverse(sort.StringSlice(files)))
+
+ buf.WriteString("<html><body><table>")
+
+ for idx := range files {
+ var (
+ file = files[idx]
+ base = strings.TrimSuffix(file, ".mp4")
+ jpg = fmt.Sprintf("%s.jpg", base)
+ jpgPath = fmt.Sprintf("%s/%s", dir, jpg)
+ )
+
+ if _, err := os.Open(jpgPath); err != nil {
+ jpg = args.DefaultThumb
+ }
+
+ if col == 0 {
+ buf.WriteString("<tr>")
+ } else if col%4 == 0 {
+ buf.WriteString("</tr>\n<tr>")
+ col = 0
+ }
+ col++
+
+ buf.WriteString(fmt.Sprintf(`
+<td><a target="_blank" href="%s"><img style="max-width: 300px" src="%s" /></a></td>
+`,
+ url.PathEscape(file),
+ url.PathEscape(jpg),
+ ))
+ }
+
+ buf.WriteString(fmt.Sprintf(
+ "</table><p>Generated %s</p></body></html>",
+ time.Now(),
+ ))
+
+ if err := os.WriteFile(args.Output, buf.Bytes(), 0o644); err != nil {
+ log.Fatal(err)
+ }
+}