From c6abeb08768566dcdac5b9adb57207814472d8f5 Mon Sep 17 00:00:00 2001 From: Evgeny Kuznetsov Date: Fri, 29 Apr 2022 17:57:45 +0300 Subject: don't die on smotrim.ru redirects --- main.go | 162 +++- main_test.go | 60 ++ testdata/TestPopulateFeed.golden | 152 ++++ testdata/smotrim.57083 | 1824 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 2169 insertions(+), 29 deletions(-) create mode 100644 testdata/TestPopulateFeed.golden create mode 100644 testdata/smotrim.57083 diff --git a/main.go b/main.go index e49bc83..24cdcb6 100644 --- a/main.go +++ b/main.go @@ -22,6 +22,7 @@ import ( "io/ioutil" "log" "net/http" + "net/url" "regexp" "strconv" "strings" @@ -77,8 +78,10 @@ func processURL(url string) *feeds.Feed { feed := getFeed(url) var wg sync.WaitGroup - wg.Add(1) - go describeFeed(feed, &wg) + if feed.Description == "" { + wg.Add(1) + go describeFeed(feed, &wg) + } describeEpisodes(feed) wg.Wait() @@ -100,11 +103,11 @@ func writeFile(output []byte, filename string) { } func getFeed(url string) (feed *feeds.Feed) { + page, url := getPage(url) feed = &feeds.Feed{ Link: &feeds.Link{Href: url}, } - page := getPage(url) if err := populateFeed(feed, page); err != nil { err = fmt.Errorf("could not process %v: %w", url, err) log.Fatal(err) @@ -114,43 +117,121 @@ func getFeed(url string) (feed *feeds.Feed) { } func populateFeed(feed *feeds.Feed, page []byte) (err error) { - title, err := parseSingle(page, programNameRe) + feed.Title, err = parseText(page, ".brand-main-item__title") + if feed.Title == "" { + feed.Title, err = parseProgrammeTitle(page) + } + if err != nil { return fmt.Errorf("bad programme page: title not found") } - feed.Title = stripLink(string(title)) - addFeedImage(page, feed) + feed.Description, _ = parseText(page, ".program-about__text") - episodes := findEpisodes(page) - urlPrefix := episodeURLPrefix(feed.Link.Href) + addFeedImage(page, feed) - for _, episode := range episodes { - if len(episodeUrlRe.FindAllSubmatch(episode, -1)) > 1 { - return errBadEpisode + switch site := parseSite(feed); site { + case "smotrim.ru": + err = populateSmotrimEpisodes(feed, page) + default: + episodes := findEpisodes(page) + urlPrefix := episodeURLPrefix(feed.Link.Href) + + for _, episode := range episodes { + if len(episodeUrlRe.FindAllSubmatch(episode, -1)) > 1 { + return errBadEpisode + } + url, err := parseSingle(episode, episodeUrlRe) + if err != nil { + return errBadEpisode + } + episodeUrl := urlPrefix + string(url) + title, _ := parseSingle(episode, episodeTitleRe) + episodeTitle := string(title) + enclosure := findEnclosure(episode) + date := findDate(episode) + + feed.Add(&feeds.Item{ + Id: episodeID(episodeUrl), + Link: &feeds.Link{Href: episodeUrl}, + Title: episodeTitle, + Enclosure: enclosure, + Created: date, + }) } - url, err := parseSingle(episode, episodeUrlRe) + } + return +} + +func populateSmotrimEpisodes(feed *feeds.Feed, page []byte) (err error) { + doc, err := goquery.NewDocumentFromReader(bytes.NewReader(page)) + if err != nil { + return + } + base, err := url.Parse(feed.Link.Href) + if err != nil { + return + } + doc.Find(".episode-card").Each(func(i int, s *goquery.Selection) { + l, _ := s.Find(".episode-card__link").Attr("href") + id := strings.TrimPrefix(l, "/audio/") + link, err := base.Parse(l) if err != nil { - return errBadEpisode + return } - episodeUrl := urlPrefix + string(url) - title, _ := parseSingle(episode, episodeTitleRe) - episodeTitle := string(title) - enclosure := findEnclosure(episode) - date := findDate(episode) - + title := strings.TrimSpace(strings.TrimPrefix(s.Find(".episode-card__title").Text(), s.Find(".episode-card__title__brand").Text())) feed.Add(&feeds.Item{ - Id: episodeID(episodeUrl), - Link: &feeds.Link{Href: episodeUrl}, - Title: episodeTitle, - Enclosure: enclosure, - Created: date, + Id: id, + Link: &feeds.Link{Href: link.String()}, + Title: title, + Enclosure: enclosure(id), }) + }) + return +} + +func parseSite(feed *feeds.Feed) string { + u, err := url.Parse(feed.Link.Href) + if err != nil { + return "" + } + return u.Hostname() +} + +func parseProgrammeTitle(page []byte) (title string, err error) { + t, err := parseSingle(page, programNameRe) + if err != nil { + return } + title = stripLink(string(t)) + return +} + +func parseText(page []byte, sel string) (title string, err error) { + doc, err := goquery.NewDocumentFromReader(bytes.NewReader(page)) + if err != nil { + return + } + title = strings.TrimSpace(doc.Find(sel).Text()) return } func addFeedImage(page []byte, feed *feeds.Feed) { + doc, err := goquery.NewDocumentFromReader(bytes.NewReader(page)) + if err != nil { + return + } + img := doc.Find(".brand-main-item__picture").Find("img") + if src, ok := img.Attr("src"); ok { + t, _ := img.Attr("title") + feed.Image = &feeds.Image{ + Link: feed.Link.Href, + Url: src, + Title: t, + } + return + } + programImage, err := parse(page, programImageRe, 4) if err == nil { feed.Image = &feeds.Image{ @@ -209,7 +290,12 @@ func findEnclosure(ep []byte) *feeds.Enclosure { return &feeds.Enclosure{} } - url := "https://audio.vgtrk.com/download?id=" + string(res) + return enclosure(string(res)) +} + +func enclosure(no string) *feeds.Enclosure { + + url := "https://audio.vgtrk.com/download?id=" + string(no) return &feeds.Enclosure{ Url: url, @@ -227,7 +313,7 @@ func findEpisodes(page []byte) [][]byte { func describeFeed(feed *feeds.Feed, wg *sync.WaitGroup) { defer wg.Done() url := strings.TrimSuffix(feed.Link.Href, "episodes") + "about" - page := getPage(url) + page, _ := getPage(url) desc, err := processFeedDesc(page) if err != nil { log.Printf("could not find programme description on page %v: %v", url, err) @@ -255,12 +341,29 @@ func describeEpisodes(feed *feeds.Feed) { func describeEpisode(item *feeds.Item, wg *sync.WaitGroup) { defer wg.Done() - page := getPage(item.Link.Href) + page, _ := getPage(item.Link.Href) desc, err := processEpisodeDesc(page) if err != nil { log.Printf("could not find episode description on page %v: %v", item.Link.Href, err) } item.Description = desc + if item.Created.IsZero() { + item.Created = parseSmotrimDate(page) + } +} + +func parseSmotrimDate(page []byte) (t time.Time) { + s, err := parseText(page, ".video__date") + if err != nil { + return + } + mnths := [12]string{"января", "февраля", "марта", "апреля", "мая", "июня", "июля", "августа", "сентября", "октября", "ноября", "декабря"} + for i, mnt := range mnths { + s = strings.ReplaceAll(s, mnt, strconv.Itoa(i+1)) + } + s = fmt.Sprintf("%s z+03", s) + t, _ = time.Parse("2 1 2006, 15:04 z-07", s) + return } func processEpisodeDesc(page []byte) (string, error) { @@ -271,6 +374,7 @@ func processEpisodeDesc(page []byte) (string, error) { var r []string r = addText(r, doc.Find(".brand-episode__head").Find(".anons").Text()) r = addText(r, doc.Find(".brand-episode__body").Find(".body").Text()) + r = addText(r, strings.TrimSpace(doc.Find(".video__body").Text())) res := strings.Join(r, "\n\n") if res == "" { @@ -286,7 +390,7 @@ func addText(arr []string, str string) []string { return arr } -func getPage(pageUrl string) []byte { +func getPage(pageUrl string) ([]byte, string) { client := &http.Client{} req, err := http.NewRequest("GET", pageUrl, nil) if err != nil { @@ -305,7 +409,7 @@ func getPage(pageUrl string) []byte { page = cleanText(page) - return page + return page, res.Request.URL.String() } // cleanText replaces HTML-encoded symbols with proper UTF diff --git a/main_test.go b/main_test.go index 6c5c172..82dc506 100644 --- a/main_test.go +++ b/main_test.go @@ -166,6 +166,25 @@ func TestUpdatingFeed(t *testing.T) { assertGolden(t, actual, golden) } +func TestPopulateFeed(t *testing.T) { + var page []byte + + feed := &feeds.Feed{ + Link: &feeds.Link{Href: "https://smotrim.ru/brand/57083"}, + } + + page = helperLoadBytes(t, "smotrim.57083") + page = cleanText(page) + + if err := populateFeed(feed, page); err != nil { + t.Fatal(err) + } + + actual := createFeed(feed) + golden := filepath.Join("testdata", t.Name()+".golden") + assertGolden(t, actual, golden) +} + func TestMissingEpisode(t *testing.T) { server := helperMockServer(t) defer helperCleanupServer(t) @@ -245,6 +264,47 @@ func BenchmarkServedFeed(b *testing.B) { } } +func TestGetFeed(t *testing.T) { + radiorus := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + page := helperLoadBytes(t, "episodes") + _, _ = w.Write(page) + })) + defer radiorus.Close() + + smotrim := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + page := helperLoadBytes(t, "smotrim.57083") + _, _ = w.Write(page) + })) + defer smotrim.Close() + + redir := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, smotrim.URL, 301) + })) + defer redir.Close() + + tests := map[string]struct { + url string + want string + desc bool + }{ + "radiorus": {radiorus.URL, radiorus.URL, false}, + "smotrim": {redir.URL, smotrim.URL, true}, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + feed := getFeed(tc.url) + if tc.want != feed.Link.Href { + t.Fatalf("\nwant %s, got %s", tc.want, feed.Link.Href) + } + ne := feed.Description != "" + if ne != tc.desc { + t.Fatalf("\nwant %v, got %v", tc.desc, ne) + } + }) + } +} + func helperMockServer(t testing.TB) *httptest.Server { t.Helper() diff --git a/testdata/TestPopulateFeed.golden b/testdata/TestPopulateFeed.golden new file mode 100644 index 0000000..221c9fe --- /dev/null +++ b/testdata/TestPopulateFeed.golden @@ -0,0 +1,152 @@ + + + Аэростат + https://smotrim.ru/brand/57083 + Вы не можете быть до конца уверены, что на этот раз вам откроет БГ – будь то взгляд на группу Doors или столь глобальные вопросы, как: что такое новое время, как делится история мира в соответствии с древней индийской космогонией, стоит ли ждать ветра перемен, ждет ли нас духовное возрождение, где граница между прошлым и будущим. А может и вовсе не стоит искать ответы на эти вопросы? Потому что это не те вопросы, а потому и ответы не приведут вас к истине... Прислушаемся к Борису Гребенщикову, который с улыбкой говорит всем нам "Здравствуйте!" и находит самые простые ответы... + + https://cdnapi.smotrim.ru/api/v1/pictures/1246171/mw/redirect + Аэростат + https://smotrim.ru/brand/57083 + + + Выпуск 884. Ответы на Вопросы + https://smotrim.ru/audio/2628425 + + + 2628425 + + + Выпуск 883. Judy Collins Wildflowers + https://smotrim.ru/audio/2627161 + + + 2627161 + + + Выпуск 882. Новое То да Сё + https://smotrim.ru/audio/2625692 + + + 2625692 + + + Выпуск 881. Новые Песни Апреля + https://smotrim.ru/audio/2624678 + + + 2624678 + + + Выпуск 880. Что такое концерт + https://smotrim.ru/audio/2623811 + + + 2623811 + + + Выпуск 879. То да Сё № 22 + https://smotrim.ru/audio/2622374 + + + 2622374 + + + Выпуск 878. Целительная сила музыки + https://smotrim.ru/audio/2621443 + + + 2621443 + + + Выпуск 877. Новые Песни Марта + https://smotrim.ru/audio/2619992 + + + 2619992 + + + Выпуск 876. Шаманизм + https://smotrim.ru/audio/2618401 + + + 2618401 + + + Выпуск 875. Новые Имена + https://smotrim.ru/audio/2617736 + + + 2617736 + + + Выпуск 874. То да сё № 21 + https://smotrim.ru/audio/2615992 + + + 2615992 + + + Выпуск 873. Новые Песни Февраля + https://smotrim.ru/audio/2614575 + + + 2614575 + + + Выпуск 872. Имболк: Романтизм + https://smotrim.ru/audio/2613623 + + + 2613623 + + + Выпуск 871. Кто Есть Кто? + https://smotrim.ru/audio/2611838 + + + 2611838 + + + Выпуск 870. То и Сё № 20 + https://smotrim.ru/audio/2610401 + + + 2610401 + + + Выпуск 869. Новые Песни Января + https://smotrim.ru/audio/2607924 + + + 2607924 + + + Выпуск 868. Новогодние притчи + https://smotrim.ru/audio/2607923 + + + 2607923 + + + Выпуск 867. Предновогодняя + https://smotrim.ru/audio/2606839 + + + 2606839 + + + Выпуск 866. Роберт Плант и Элисон Краусс + https://smotrim.ru/audio/2605330 + + + 2605330 + + + Выпуск 865. 8 Наблюдений + https://smotrim.ru/audio/2603883 + + + 2603883 + + + \ No newline at end of file diff --git a/testdata/smotrim.57083 b/testdata/smotrim.57083 new file mode 100644 index 0000000..82e63c8 --- /dev/null +++ b/testdata/smotrim.57083 @@ -0,0 +1,1824 @@ + + + + + + Аэростат, радио, программа, слушать онлайн // Смотрим + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + +
+ + + + + + + +
+
+ +
+
+
+ +
+
+ + +
+
+
+
+
+
+
+
+
+
+
Выберите регион
+
+
    +
  • + + +
  • +
  • + +
    + + +
    +
  • +
+
+
+ +
+
+
+
+
+
+ + + +
+
+
+ + + + +
+
+
+
+ +
+
+
+
+

Смотрим на

+ +
+
+
+
+ +
+
+
+ +
+ +
+
+ + +
+
+ + + + + + + Аэростат + + + + + + +
+
+
+
+ Программа +
+

+ Аэростат +

+
+ 0+ +
+
+ + +
+
+ + + + + + + + + +
+ +
+

+ + Слушаем + +

+
+ +
+
+
    +
  • + +
    +
    + +
    +

    + + Аэростат + +

    +

    + + Выпуск 884. Ответы на Вопросы + +

    + + + +
    +
    + 46:01 +
    +
    +
    +
    + +
  • +
  • + +
    +
    + +
    +

    + + Аэростат + +

    +

    + + Выпуск 883. Judy Collins Wildflowers + +

    + + + +
    +
    + 46:01 +
    +
    +
    +
    + +
  • +
  • + +
    +
    + +
    +

    + + Аэростат + +

    +

    + + Выпуск 882. Новое То да Сё + +

    + + + +
    +
    + 46:01 +
    +
    +
    +
    + +
  • +
  • + +
    +
    + +
    +

    + + Аэростат + +

    +

    + + Выпуск 881. Новые Песни Апреля + +

    + + + +
    +
    + 46:01 +
    +
    +
    +
    + +
  • +
  • + +
    +
    + +
    +

    + + Аэростат + +

    +

    + + Выпуск 880. Что такое концерт + +

    + + + +
    +
    + 46:01 +
    +
    +
    +
    + +
  • +
  • + +
    +
    + +
    +

    + + Аэростат + +

    +

    + + Выпуск 879. То да Сё № 22 + +

    + + + +
    +
    + 46:01 +
    +
    +
    +
    + +
  • +
  • + +
    +
    + +
    +

    + + Аэростат + +

    +

    + + Выпуск 878. Целительная сила музыки + +

    + + + +
    +
    + 46:01 +
    +
    +
    +
    + +
  • +
  • + +
    +
    + +
    +

    + + Аэростат + +

    +

    + + Выпуск 877. Новые Песни Марта + +

    + + + +
    +
    + 46:01 +
    +
    +
    +
    + +
  • +
  • + +
    +
    + +
    +

    + + Аэростат + +

    +

    + + Выпуск 876. Шаманизм + +

    + + + +
    +
    + 46:01 +
    +
    +
    +
    + +
  • +
  • + +
    +
    + +
    +

    + + Аэростат + +

    +

    + + Выпуск 875. Новые Имена + +

    + + + +
    +
    + 46:01 +
    +
    +
    +
    + +
  • +
  • + +
    +
    + +
    +

    + + Аэростат + +

    +

    + + Выпуск 874. То да сё № 21 + +

    + + + +
    +
    + 46:01 +
    +
    +
    +
    + +
  • +
  • + +
    +
    + +
    +

    + + Аэростат + +

    +

    + + Выпуск 873. Новые Песни Февраля + +

    + + + +
    +
    + 46:01 +
    +
    +
    +
    + +
  • +
  • + +
    +
    + +
    +

    + + Аэростат + +

    +

    + + Выпуск 872. Имболк: Романтизм + +

    + + + +
    +
    + 46:01 +
    +
    +
    +
    + +
  • +
  • + +
    +
    + +
    +

    + + Аэростат + +

    +

    + + Выпуск 871. Кто Есть Кто? + +

    + + + +
    +
    + 46:01 +
    +
    +
    +
    + +
  • +
  • + +
    +
    + +
    +

    + + Аэростат + +

    +

    + + Выпуск 870. То и Сё № 20 + +

    + + + +
    +
    + 46:01 +
    +
    +
    +
    + +
  • +
  • + +
    +
    + +
    +

    + + Аэростат + +

    +

    + + Выпуск 869. Новые Песни Января + +

    + + + +
    +
    + 46:01 +
    +
    +
    +
    + +
  • +
  • + +
    +
    + +
    +

    + + Аэростат + +

    +

    + + Выпуск 868. Новогодние притчи + +

    + + + +
    +
    + 46:01 +
    +
    +
    +
    + +
  • +
  • + +
    +
    + +
    +

    + + Аэростат + +

    +

    + + Выпуск 867. Предновогодняя + +

    + + + +
    +
    + 46:01 +
    +
    +
    +
    + +
  • +
  • + +
    +
    + +
    +

    + + Аэростат + +

    +

    + + Выпуск 866. Роберт Плант и Элисон Краусс + +

    + + + +
    +
    + 46:01 +
    +
    +
    +
    + +
  • +
  • + +
    +
    + +
    +

    + + Аэростат + +

    +

    + + Выпуск 865. 8 Наблюдений + +

    + + + +
    +
    + 46:01 +
    +
    +
    +
    + +
  • +
+
+ +
+ + +
+
+ +
+
+
+
О программе
+
+
+

Вы не можете быть до конца уверены, что на этот раз вам откроет БГ – будь то взгляд на группу Doors или столь глобальные вопросы, как: что такое новое время, как делится история мира в соответствии с древней индийской космогонией, стоит ли ждать ветра перемен, ждет ли нас духовное возрождение, где граница между прошлым и будущим. А может и вовсе не стоит искать ответы на эти вопросы? Потому что это не те вопросы, а потому и ответы не приведут вас к истине...

+ +

Прислушаемся к Борису Гребенщикову, который с улыбкой говорит всем нам "Здравствуйте!" и находит самые простые ответы...

+
+
+
+ + + + + + + + + + + + +
+ + + + + + + + + + + + +
+ + + + + + +
+ + + + + + +
+ +
+ + + + + +
+ + + + + + +
+
+ +
+ +
+ + + + + + +
+
+
+

Персоны

+
+
+ +
+
+
+ + +
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+ + + + + + + +
+
+ +

+ + Слушаем также + +

+ +
+
+
+
    + +
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+ +
+ + + + + +
+
+ + + + + + + + + +
+
+
+ + + +
+
+ +
+
+
+
+ +
+

+
+
+ + +
+
+
+ + + + + +
+
+ + + +
+
+
+ +
+
+
+ + +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + +
+
+
+
+ +
+
+
+ + + \ No newline at end of file -- cgit v1.2.3