diff --git a/.gitignore b/.gitignore index 916cd2e..58ce334 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ fabric.properties tmp/ vendor/ config/secret.yaml -*.md \ No newline at end of file +*.md +mdb \ No newline at end of file diff --git a/pkg/repository/rutracker.go b/pkg/repository/rutracker.go index fbf280c..539bf4a 100644 --- a/pkg/repository/rutracker.go +++ b/pkg/repository/rutracker.go @@ -42,22 +42,19 @@ func (s *Rutracker) GetTopic(topics []string) ([]model.ExternalSources, error) { for _, e := range topic.Entry { var es model.ExternalSources - u, _ := url.Parse(e.Link.Href) - es.Type = constant.ScopeRuTracker - es.TypeId, _ = strconv.Atoi(u.Query().Get("t")) + es.TypeId = topicId(e) es.Title = e.Title es.TypeSubsectionId, _ = strconv.Atoi(t) es.Releaser = e.Author.Name es.Created = carbon.Parse(e.Updated, "Europe/Kyiv") esModel := table.ExternalSources{Columns: columns} + + fmt.Printf("%+v\n\n\n", es) + entry := esModel.InsertOnDuplicate(es, s.db) entries = append(entries, entry) - - ///fmt.Println(entries) - - fmt.Printf("%+v\n\n\n", entry) } } @@ -67,22 +64,44 @@ func (s *Rutracker) GetTopic(topics []string) ([]model.ExternalSources, error) { func fetch(endpoint string) (*resource.RutrackerAtomTopic, error) { resp, err := http.Get(endpoint) if err != nil { - slog.Error("couldn't fetch data", endpoint, err.Error()) + slog.Error("couldn't fetch data", "endpoint", endpoint, "err", err.Error()) return nil, err } defer func(Body io.ReadCloser) { - err = Body.Close() - if err != nil { - fmt.Println("Body.Close") + if cerr := Body.Close(); cerr != nil { + slog.Error("couldn't close response body", "endpoint", endpoint, "err", cerr.Error()) } }(resp.Body) + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code %d from %s", resp.StatusCode, endpoint) + } + topic := &resource.RutrackerAtomTopic{} if err = xml.NewDecoder(resp.Body).Decode(topic); err != nil { + slog.Error("couldn't decode feed", "endpoint", endpoint, "err", err.Error()) return nil, err } return topic, nil } + +// topicId resolves the rutracker topic id preferring the page link query (?t=...), +// falling back to the entry id tag (tag:rto.feed,...:/t/5702203). +func topicId(e resource.RutrackerAtomEntry) int { + if link, ok := e.PageLink(); ok { + if u, err := url.Parse(link.Href); err == nil { + if id, err := strconv.Atoi(u.Query().Get("t")); err == nil { + return id + } + } + } + id, err := e.TopicId() + if err != nil { + slog.Error("couldn't resolve topic id", "entry_id", e.ID, "err", err.Error()) + return 0 + } + return id +} diff --git a/types/resource/rutracker.go b/types/resource/rutracker.go index cf2e1d2..c6910d6 100644 --- a/types/resource/rutracker.go +++ b/types/resource/rutracker.go @@ -1,35 +1,61 @@ package resource -import "encoding/xml" +import ( + "encoding/xml" + "fmt" + "strconv" + "strings" +) +// RutrackerAtomTopic represents the atom feed of a rutracker forum subsection. type RutrackerAtomTopic struct { - XMLName xml.Name `xml:"feed"` - Text string `xml:",chardata"` - Xmlns string `xml:"xmlns,attr"` - ID string `xml:"id"` - Link struct { - Text string `xml:",chardata"` - Href string `xml:"href,attr"` - } `xml:"link"` - Updated string `xml:"updated"` - Title string `xml:"title"` - Entry []struct { - Text string `xml:",chardata"` - ID string `xml:"id"` - Link struct { - Text string `xml:",chardata"` - Href string `xml:"href,attr"` - } `xml:"link"` - Updated string `xml:"updated"` - Title string `xml:"title"` - Author struct { - Text string `xml:",chardata"` - Name string `xml:"name"` - } `xml:"author"` - Category struct { - Text string `xml:",chardata"` - Term string `xml:"term,attr"` - Label string `xml:"label,attr"` - } `xml:"category"` - } `xml:"entry"` + XMLName xml.Name `xml:"feed"` + Xmlns string `xml:"xmlns,attr"` + ID string `xml:"id"` + Link []AtomLink `xml:"link"` + Updated string `xml:"updated"` + Title string `xml:"title"` + Entry []RutrackerAtomEntry `xml:"entry"` +} + +// AtomLink is a generic atom link element. An entry contains two links: +// a topic page link (no rel) and a magnet link (rel="enclosure"). +type AtomLink struct { + Href string `xml:"href,attr"` + Rel string `xml:"rel,attr"` + Type string `xml:"type,attr"` +} + +type RutrackerAtomEntry struct { + ID string `xml:"id"` + Link []AtomLink `xml:"link"` + Updated string `xml:"updated"` + Title string `xml:"title"` + Author struct { + Name string `xml:"name"` + } `xml:"author"` + Category struct { + Term string `xml:"term,attr"` + Label string `xml:"label,attr"` + } `xml:"category"` +} + +// PageLink returns the link to the topic page (the one without rel attribute). +func (e RutrackerAtomEntry) PageLink() (AtomLink, bool) { + for _, l := range e.Link { + if l.Rel == "" { + return l, true + } + } + return AtomLink{}, false +} + +// TopicId extracts the topic id from the entry id tag: "tag:rto.feed,2026-08-29:/t/5702203". +func (e RutrackerAtomEntry) TopicId() (int, error) { + const marker = ":/t/" + i := strings.LastIndex(e.ID, marker) + if i < 0 { + return 0, fmt.Errorf("unexpected entry id format: %s", e.ID) + } + return strconv.Atoi(e.ID[i+len(marker):]) }