Compare commits

...

17 Commits

Author SHA1 Message Date
bel
18ac13fd57 should not execute feed if version.url=="" 2025-12-10 08:40:28 -07:00
bel
375fc1000a feeds for each runs all and returns all errs 2025-12-10 08:38:59 -07:00
bel
47c7aa74d3 uncap retries on main 2025-12-10 08:23:54 -07:00
bel
a6aad2820d cap retries 2025-12-10 08:13:45 -07:00
bel
b26afcb325 log 2025-12-02 16:30:10 -07:00
bel
613bfdf96e transcode entrypoint 2025-12-02 16:28:55 -07:00
Bel LaPointe
81507319dd disable by default 2025-12-02 16:25:15 -07:00
bel
50ad3bb3db install_scratch.sh 2025-11-30 09:00:47 -07:00
bel
07992b6636 delete less accidentally clickable 2025-11-30 08:56:37 -07:00
bel
9583234df5 do not list deleted (url == "") 2025-11-30 08:54:28 -07:00
bel
2943362587 if POST /?delete then DELETE 2025-11-30 08:54:18 -07:00
bel
cbd4e32022 DELETE /v1/feeds/abc updates all fields to "" 2025-11-30 08:44:27 -07:00
bel
727b4fdea6 from testdata to public for .html 2025-11-30 08:40:15 -07:00
bel
fd7dcafd4e timeout ass to srt 2025-06-01 10:28:43 -06:00
Bel LaPointe
4fbc96b96f dont fail on err but log all 2025-06-01 10:27:00 -06:00
Bel LaPointe
0afb6535b6 impl -e=best-ass-to-srt 2025-06-01 09:55:01 -06:00
Bel LaPointe
10a40d4a54 nopanik choose best lang 2025-06-01 09:52:18 -06:00
13 changed files with 252 additions and 71 deletions

3
install_scratch.sh Normal file
View File

@@ -0,0 +1,3 @@
#! /usr/bin/env bash
CGO_ENABLED=1 CC=x86_64-linux-musl-gcc go install -ldflags="-linkmode external -extldflags '-static'"

View File

@@ -12,6 +12,7 @@ import (
"regexp"
"slices"
"strings"
"time"
)
func Entrypoint(ctx context.Context, p string) error {
@@ -171,6 +172,9 @@ func BestAssToSRT(ctx context.Context, p string) error {
}
func assToSRT(ctx context.Context, ass string) (string, error) {
ctx, can := context.WithTimeout(ctx, 30*time.Second)
defer can()
srt := fmt.Sprintf("%s.srt", strings.TrimSuffix(ass, ".ass"))
if _, err := os.Stat(srt); err == nil {
return srt, nil
@@ -198,27 +202,45 @@ func assToSRT(ctx context.Context, ass string) (string, error) {
}
func SRTsByGoodness(srts []string) []string {
/*
1 lat.*amer Lat.*Amer \
2 signs \
3 rus Rus \
4 por Por \
5 ita Ita \
6 fre Fre \
7 spa Spa \
8 ger Ger \
9 ara Ara \
10 jpn Jpn \
11 Europ \
12 Brazil \
13 Deu \
*/
panic("NOT IMPL")
skippers := []*regexp.Regexp{
regexp.MustCompile(`(?i)lat.*amer`),
regexp.MustCompile(`(?i)signs`),
regexp.MustCompile(`(?i)rus`),
regexp.MustCompile(`(?i)por`),
regexp.MustCompile(`(?i)ita`),
regexp.MustCompile(`(?i)fre`),
regexp.MustCompile(`(?i)spa`),
regexp.MustCompile(`(?i)ger`),
regexp.MustCompile(`(?i)ara`),
regexp.MustCompile(`(?i)jpn`),
regexp.MustCompile(`(?i)urop`),
regexp.MustCompile(`(?i)razil`),
regexp.MustCompile(`(?i)Deu`),
regexp.MustCompile(`(?i)ara`),
}
keepers := []*regexp.Regexp{
regexp.MustCompile(`(?i)^eng$`),
}
srts = slices.Clone(srts)
slices.SortFunc(srts, func(a, b string) int {
// if skip a { return 1 }
// if skip b { return -1 }
// return -1 * (wc(a) - wc(b))
a = strings.ToLower(a)
b = strings.ToLower(b)
for _, skipper := range skippers {
if skipper.MatchString(b) {
return -1
} else if skipper.MatchString(a) {
return 1
}
}
for _, keeper := range keepers {
if keeper.MatchString(a) {
return -1
} else if keeper.MatchString(b) {
return 1
}
}
return strings.Compare(a, b)
})
return srts

View File

@@ -14,6 +14,24 @@ func TestSRTsByGoodness(t *testing.T) {
given: []string{"a", "eng"},
want: "eng",
},
"eng nocap": {
given: []string{"A", "eng"},
want: "eng",
},
".Apothecary_Diaries_S02E19.mkv.0:9.ita.ass": {
given: []string{
".Apothecary_Diaries_S02E19.mkv.0:10.rus.srt",
".Apothecary_Diaries_S02E19.mkv.0:2.eng.srt",
".Apothecary_Diaries_S02E19.mkv.0:3.por.srt",
".Apothecary_Diaries_S02E19.mkv.0:4.spa.srt",
".Apothecary_Diaries_S02E19.mkv.0:5.spa.srt",
".Apothecary_Diaries_S02E19.mkv.0:6.ara.srt",
".Apothecary_Diaries_S02E19.mkv.0:7.fre.srt",
".Apothecary_Diaries_S02E19.mkv.0:8.ger.srt",
".Apothecary_Diaries_S02E19.mkv.0:9.ita.srt",
},
want: ".Apothecary_Diaries_S02E19.mkv.0:2.eng.srt",
},
}
for name, d := range cases {

View File

@@ -12,8 +12,9 @@ import (
"path"
"show-rss/src/slow"
"strconv"
"golang.org/x/time/rate"
"time"
"golang.org/x/time/rate"
)
var EnvCksumBPS = func() int {
@@ -81,6 +82,11 @@ func One(ctx context.Context, p string) error {
return err
}
log.Printf("asses.transcode(%s)...", shortp)
if err := transcode(ctx, p); err != nil {
return err
}
return nil
}(); err != nil {
return err

View File

@@ -12,6 +12,8 @@ import (
func TestOne(t *testing.T) {
ctx := db.Test(t, context.Background())
os.Setenv("DO_TRANSCODE", "true")
d := t.TempDir()
b, _ := os.ReadFile(path.Join("testdata", "survivor_au_S11E12.smoller.mkv"))
p := path.Join(d, "f.mkv")

64
src/asses/transcode.go Normal file
View File

@@ -0,0 +1,64 @@
package asses
import (
"context"
"fmt"
"log"
"os"
"path"
"slices"
"strings"
)
func EntrypointTranscode(ctx context.Context, p string) error {
return transcode(ctx, p)
}
func transcode(ctx context.Context, p string) error {
if os.Getenv("NO_TRANSCODE") != "" || os.Getenv("DO_TRANSCODE") == "" {
log.Printf("would transcode %s but $NO_TRANSCODE=x or $DO_TRANSCODE=", p)
return nil
}
output, err := ffprobe(ctx, "-i", p)
if err != nil {
return err
}
h264 := slices.ContainsFunc(strings.Split(output, "\n"), func(line string) bool {
return strings.Contains(line, "tream #") && strings.Contains(line, "Video: ") && strings.Contains(line, "h264")
})
aac := slices.ContainsFunc(strings.Split(output, "\n"), func(line string) bool {
return strings.Contains(line, "tream #") && strings.Contains(line, "Audio: ") && strings.Contains(line, "aac")
})
if h264 && aac {
return nil
}
p2 := p + ".en" + path.Ext(p)
if err := ffmpeg(ctx, "-y",
"-i", p,
"-vcodec", "libx264",
"-acodec", "aac",
p2,
); err != nil {
return err
}
output2, err := ffprobe(ctx, "-i", p)
if err != nil {
return err
}
df := func(line string) bool {
return !strings.Contains(line, "tream #")
}
originalStreams := slices.DeleteFunc(strings.Split(output, "\n"), df)
newStreams := slices.DeleteFunc(strings.Split(output2, "\n"), df)
if len(originalStreams) != len(newStreams) {
return fmt.Errorf("stream count changed from transcode")
}
return os.Rename(p2, p)
}

View File

@@ -47,6 +47,27 @@ func Main(ctx context.Context, args []string) error {
}
}
return nil
case BestAssToSRT:
errs := []string{}
for _, pos := range flags.Pos {
if err := inass.BestAssToSRT(ctx, pos); err != nil {
err = fmt.Errorf("[%s] %w", pos, err)
log.Println(err)
errs = append(errs, err.Error())
}
}
if len(errs) > 0 {
return fmt.Errorf("errors: %+v", errs)
}
return nil
case Transcode:
for _, pos := range flags.Pos {
log.Printf("transcoding %q...", pos)
if err := inass.EntrypointTranscode(ctx, pos); err != nil {
return err
}
}
return nil
default:
panic(flags.Entrypoint.String())
}
@@ -58,6 +79,8 @@ type Entrypoint int
const (
Defacto Entrypoint = iota
DeportAss
BestAssToSRT
Transcode
)
func (e *Entrypoint) Set(s string) error {
@@ -66,10 +89,16 @@ func (e *Entrypoint) Set(s string) error {
*e = Defacto
case DeportAss.String():
*e = DeportAss
case BestAssToSRT.String():
*e = BestAssToSRT
case Transcode.String():
*e = Transcode
default:
return fmt.Errorf("%s nin (%s)", s, strings.Join([]string{
Defacto.String(),
DeportAss.String(),
BestAssToSRT.String(),
Transcode.String(),
}, ", "))
}
return nil
@@ -81,6 +110,10 @@ func (e Entrypoint) String() string {
return ""
case DeportAss:
return "deport-ass"
case BestAssToSRT:
return "best-ass-to-srt"
case Transcode:
return "transcode"
}
panic("cannot serialize entrypoint")
}
@@ -99,6 +132,5 @@ func runner(ctx context.Context, k string, foo func(context.Context) error) func
case <-time.After(time.Second):
}
}
return err
}
}

View File

@@ -10,13 +10,26 @@ import (
func (h Handler) feeds(w http.ResponseWriter, r *http.Request) error {
switch r.Method {
case http.MethodDelete:
if err := r.ParseForm(); err != nil {
return err
}
if err := h.feedsDelete(r.Context(), r.URL.Query().Get("id")); err != nil {
return err
}
case http.MethodPost, http.MethodPut:
if err := r.ParseForm(); err != nil {
return err
}
if r.URL.Query().Has("delete") {
if err := h.feedsDelete(r.Context(), r.URL.Query().Get("id")); err != nil {
return err
}
} else {
if err := h.feedsUpsert(r.Context(), r.URL.Query().Get("id"), r.Form); err != nil {
return err
}
}
default:
http.NotFound(w, r)
return nil
@@ -29,6 +42,10 @@ func (h Handler) feeds(w http.ResponseWriter, r *http.Request) error {
return nil
}
func (h Handler) feedsDelete(ctx context.Context, id string) error {
return feeds.Delete(ctx, id)
}
func (h Handler) feedsUpsert(ctx context.Context, id string, form url.Values) error {
var req feeds.Version
for k, v := range map[string]*string{

View File

@@ -23,7 +23,13 @@
{{ if eq "" .editing.ID }}
New
{{ else }}
Update <code><a target="_blank" href="{{ .editing_url }}">{{ .editing.URL }}</a></code> (<a href="?">clear</a>)
Updating <code><a target="_blank" href="{{ .editing_url }}">{{ .editing.URL }}</a></code> (<a href="?">clear</a>)
<br>
<div style="scale: 0.85">
<form method="POST" action="/v1/feeds?id={{ .editing.ID }}&delete">
<button type="submit">DELETE</button>
</form>
</div>
{{ end }}
</h3>
<form method="POST" action="/v1/feeds?id={{ .editing.ID }}">

View File

@@ -17,17 +17,17 @@ import (
_ "embed"
)
//go:embed testdata/index.tmpl
//go:embed public/index.tmpl
var embeddedIndexTMPL string
//go:embed testdata/*
//go:embed public/*
var embeddedDir embed.FS
var dir = func() string {
if v := os.Getenv("UI_D"); v != "" {
return v
}
return "./src/cmd/server/handler/testdata"
return "./src/cmd/server/handler/public"
}()
func (h Handler) ui(w http.ResponseWriter, r *http.Request) error {
@@ -37,7 +37,7 @@ func (h Handler) ui(w http.ResponseWriter, r *http.Request) error {
w.Header().Set("Cache-Control", "max-age=2592000")
fs := http.FileServer(http.FS(embeddedDir))
r.URL.Path = fmt.Sprintf("/testdata/%s", strings.TrimPrefix(r.URL.Path, "/experimental/ui"))
r.URL.Path = fmt.Sprintf("/public/%s", strings.TrimPrefix(r.URL.Path, "/experimental/ui"))
fs.ServeHTTP(w, r)
return nil
}
@@ -57,7 +57,9 @@ func (h Handler) uiIndex(w http.ResponseWriter, r *http.Request) error {
editing.Version.WebhookURL = "vpntor:///data/completed-rss/TITLE"
all := []feeds.Feed{}
if err := feeds.ForEach(ctx, func(f feeds.Feed) error {
if deleted := f.Version.URL == ""; !deleted {
all = append(all, f)
}
if f.Entry.ID == r.URL.Query().Get("edit") {
editing.Entry = f.Entry
editing.Version = f.Version

View File

@@ -72,18 +72,27 @@ func ForEach(ctx context.Context, cb func(Feed) error) error {
return err
}
var errs []string
for _, id := range ids {
feed, err := Get(ctx, id.ID)
if err != nil {
return err
} else if err := cb(feed); err != nil {
return err
errs = append(errs, fmt.Sprintf(`failed to fetch %s: %v`, id.ID, err))
}
}
if len(errs) > 0 {
return fmt.Errorf("failed some callbacks: %+v", errs)
}
return nil
}
func Delete(ctx context.Context, id string) error {
return Update(ctx, id, "", "", "", "", "", "")
}
func Get(ctx context.Context, id string) (Feed, error) {
if err := initDB(ctx); err != nil {
return Feed{}, err

View File

@@ -28,7 +28,7 @@ var (
)
func (feed Feed) ShouldExecute() (bool, error) {
if !feed.Entry.Deleted.IsZero() {
if !feed.Entry.Deleted.IsZero() || feed.Version.URL == "" {
return false, nil
}
next, err := feed.Next()