"Every Episode" says what a playlist holds, not which show; a premiere to come is listed without a title until it airs.
- commit
- 189823c3efa08791159c5061d0c08adf0bb595b2
- parent
- 758b5ecc158d5f4e94988fc02b87933acec2e10e
- author
- FelineStateMachine <226808055+FelineStateMachine@users.noreply.github.com>
- kind
- fix risk score 1.9 of 3, Jev's estimate
| M | internal/ | 15 | +++++++++++++++ |
| M | internal/ | 26 | ++++++++++++++++++++++++-- |
@@ -69,6 +69,12 @@ func fakeYtDlp(args []string) int {
6969 {"id": "pbx", "title": "[Private video]"},7070 {"id": "pb2", "title": "B 2", "duration": 3600, "timestamp": now - day},7171 }}72+ case strings.Contains(u, "playlist?list=PLtest00000002"):73+ out = map[string]any{"_type": "playlist", "id": "PLtest00000002", "title": "Every Episode", "channel": "Show C", "channel_id": "UCc",74+ "entries": []map[string]any{75+ {"id": "pc0"},76+ {"id": "pc1", "title": "C 1", "duration": 3600, "timestamp": now - day},77+ }}7278 case strings.Contains(u, "results?search_query="):7379 out = map[string]any{"entries": []map[string]any{{"channel_id": "UCa", "channel": "Show A", "uploader_id": "@showa",7480 "channel_follower_count": 1000, "description": "A show."}}}@@ -184,6 +190,15 @@ func TestAddStreamsAndPlaylist(t *testing.T) {
184190 if st := l.State(); len(st.Shows) != 2 || st.Shows[0].ID != s.ID {185191 t.Errorf("state: %+v", st)186192 }193+ // A playlist titled for what it holds is named for its channel; a video194+ // without a title yet isn't an episode.195+ c, err := l.Add(context.Background(), "https://www.youtube.com/playlist?list=PLtest00000002", 0)196+ if err != nil {197+ t.Fatal(err)198+ }199+ if got := ids(c.Episodes); c.Title != "Show C" || !slices.Equal(got, []string{"pc1"}) {200+ t.Errorf("generic playlist: %q %v", c.Title, got)201+ }187202 }188203 189204 func TestBadShows(t *testing.T) {@@ -1,13 +1,16 @@
11 package podcast22 33 import (4+ "cmp"45 "context"56 "errors"67 "log"78 "math"89 "net/http"10+ "regexp"911 "slices"1012 "strconv"13+ "strings"1114 "time"1215 1316 "airwaves/internal/web"@@ -163,8 +166,8 @@ func (l *Library) relist(ctx context.Context, s *show) {
163166 // each, plus older ones part watched. Videos too short to be episodes,164167 // and streams to come or under way, are left out.165168 func (s *show) merge(lst youtube.Listing, sr src, now time.Time) {166- if s.Title == "" || lst.Name != "" {167- s.Title = lst.Name169+ if t := showTitle(lst, sr); t != "" {170+ s.Title = t168171 }169172 if lst.Image != "" {170173 s.Thumbnail = lst.Image@@ -182,6 +185,11 @@ func (s *show) merge(lst youtube.Listing, sr src, now time.Time) {
182185 var out []*episode183186 seen := map[string]bool{}184187 for _, v := range lst.Entries {188+ // A video without a title yet (a premiere to come, say) is189+ // listed once it has one.190+ if v.Title == "" {191+ continue192+ }185193 if seen[v.ID] || !s.fits(v.Seconds, v.LiveStatus, sr) {186194 if v.Seconds > 0 && float64(v.Seconds) < s.MinMinutes*60 {187195 s.skip(v.ID)@@ -241,6 +249,20 @@ func (s *show) fits(seconds int, live string, sr src) bool {
241249 return seconds <= 0 || float64(seconds) >= s.MinMinutes*60242250 }243251 252+// showTitle is what a listing calls the show: a playlist's title, unless253+// it names no show ("Every Episode", "Full Episodes", "Podcast"), then its254+// channel's name; a channel's name.255+func showTitle(lst youtube.Listing, sr src) string {256+ if sr.playlist && lst.Owner != "" && strings.TrimSpace(generic.ReplaceAllString(lst.Name, "")) == "" {257+ return lst.Owner258+ }259+ return cmp.Or(lst.Name, lst.Owner)260+}261+262+// generic are the words of a playlist's title that say what it holds, not263+// which show.264+var generic = regexp.MustCompile(`(?i)\b(the|every|all|full|latest|new|episodes?|podcasts?|shows?|uploads?|videos?)\b|[^\pL\pN]+`)265+244266 // skip remembers a video that isn't an episode.245267 func (s *show) skip(id string) {246268 if slices.Contains(s.Skipped, id) {