mirror of
https://github.com/rwinkhart/backtone.git
synced 2026-09-03 15:47:42 -04:00
Add support for APIs (in addition to scraping)
This commit is contained in:
@@ -1,50 +0,0 @@
|
|||||||
package bthtml
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type payloadT struct {
|
|
||||||
Cmd string `json:"cmd"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
MaxTimeoutMilliseconds int `json:"maxTimeout"`
|
|
||||||
WaitInSeconds float32 `json:"waitInSeconds"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetFromURL(flareSolverrURL, webPageURL string, loadSeconds float32) (string, error) {
|
|
||||||
client := &http.Client{}
|
|
||||||
|
|
||||||
payload := payloadT{
|
|
||||||
Cmd: "request.get",
|
|
||||||
URL: webPageURL,
|
|
||||||
MaxTimeoutMilliseconds: 60000,
|
|
||||||
WaitInSeconds: loadSeconds,
|
|
||||||
}
|
|
||||||
payloadBytes, err := json.Marshal(payload)
|
|
||||||
if err != nil {
|
|
||||||
return "", errors.New("unable to marshal FlareSolverr payload to JSON: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
req, _ := http.NewRequest(
|
|
||||||
"POST",
|
|
||||||
flareSolverrURL,
|
|
||||||
bytes.NewBuffer(payloadBytes),
|
|
||||||
)
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return "", errors.New("unable to make FlareSolverr request to " + flareSolverrURL + ": " + err.Error())
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return "", errors.New("unable to read FlareSolverr response body: " + err.Error())
|
|
||||||
}
|
|
||||||
return string(body), nil
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<input>
|
||||||
|
<apiMethodEndpointPayload
|
||||||
|
><![CDATA[POST@https://api.enchor.us/search@{"search":"*","per_page":10,"page":1,"instrument":null,"difficulty":null,"drumType":null,"drumsReviewed":false,"sort":null,"source":"bridge"}]]></apiMethodEndpointPayload>
|
||||||
|
<regex
|
||||||
|
><![CDATA[{\"ordering\":.*?,\"name\":\"(.*?)\",\"artist\":\"(.*?)\",\"album\":\"(.*?)\",.*?\",\"md5\":\"(.*?)\",.*?,\"charter\":\"(.*?)\",]]></regex>
|
||||||
|
<loadSeconds>1</loadSeconds>
|
||||||
|
<maxFeedItems>10</maxFeedItems>
|
||||||
|
<feed>
|
||||||
|
<title>
|
||||||
|
<parent>Chorus Encore</parent>
|
||||||
|
<captureGroupConnector />
|
||||||
|
<captureGroupIndex>1</captureGroupIndex>
|
||||||
|
<captureGroupConnector> by </captureGroupConnector>
|
||||||
|
<captureGroupIndex>2</captureGroupIndex>
|
||||||
|
</title>
|
||||||
|
<link><parent>https://www.enchor.us/</parent>
|
||||||
|
<captureGroupConnector>https://www.enchor.us/chart/</captureGroupConnector>
|
||||||
|
<captureGroupIndex>4</captureGroupIndex>
|
||||||
|
</link>
|
||||||
|
<description>
|
||||||
|
<parent>Latest YARG/CH charts published to Chorus Encore</parent>
|
||||||
|
<captureGroupConnector>Charter: </captureGroupConnector>
|
||||||
|
<captureGroupIndex>5</captureGroupIndex>
|
||||||
|
<captureGroupConnector> | Album: </captureGroupConnector>
|
||||||
|
<captureGroupIndex>3</captureGroupIndex>
|
||||||
|
</description>
|
||||||
|
<authorName>
|
||||||
|
<parent>Gamers</parent>
|
||||||
|
<captureGroupIndex>5</captureGroupIndex>
|
||||||
|
</authorName>
|
||||||
|
<authorEmail />
|
||||||
|
</feed>
|
||||||
|
</input>
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
<input>
|
<input>
|
||||||
<flareSolverrURL>http://PLACEHOLDER:8191/v1</flareSolverrURL>
|
<flareSolverrEndpoint>http://PLACEHOLDER:8191/v1</flareSolverrEndpoint>
|
||||||
<regex
|
<regex
|
||||||
><![CDATA[<ce-search-result class=\\"card rounded-none bg-base-100 shadow-xl\\"><!----><!----><div class=\\"flex\\"><img alt=\\"Album art\\" loading=\\"lazy\\" class=\\"object-cover w-40\\" src=\\".*?\\"><!----><div class=\\"flex-grow p-3\\"><div class=\\"text-xl \[text-wrap:balance\] sm:text-2xl\\">(.*?)<\/div><!----><div><span class=\\"text-sm font-bold \[text-wrap:balance\] sm:text-base\\">Album:<\/span> (.*?)<\/div><div><span class=\\"text-sm font-bold \[text-wrap:balance\] sm:text-base\\">Genre:<\/span> .*?Charter:<\/div><a target=\\"_blank\\" class=\\"link-hover link\\" href=\\".*?\\">(.*?)<\/a><\/div><!----><!----><!----><!---->.*?for=\\"downloadVideos_(.*?)\\"]]></regex>
|
><![CDATA[<ce-search-result class=\\"card rounded-none bg-base-100 shadow-xl\\"><!----><!----><div class=\\"flex\\"><img alt=\\"Album art\\" loading=\\"lazy\\" class=\\"object-cover w-40\\" src=\\".*?\\"><!----><div class=\\"flex-grow p-3\\"><div class=\\"text-xl \[text-wrap:balance\] sm:text-2xl\\">(.*?)<\/div><!----><div><span class=\\"text-sm font-bold \[text-wrap:balance\] sm:text-base\\">Album:<\/span> (.*?)<\/div><div><span class=\\"text-sm font-bold \[text-wrap:balance\] sm:text-base\\">Genre:<\/span> .*?Charter:<\/div><a target=\\"_blank\\" class=\\"link-hover link\\" href=\\".*?\\">(.*?)<\/a><\/div><!----><!----><!----><!---->.*?for=\\"downloadVideos_(.*?)\\"]]></regex>
|
||||||
<loadSeconds>1</loadSeconds>
|
<loadSeconds>1</loadSeconds>
|
||||||
@@ -5,10 +5,11 @@ import (
|
|||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gorilla/feeds"
|
"github.com/gorilla/feeds"
|
||||||
"github.com/rwinkhart/backtone/atom"
|
"github.com/rwinkhart/backtone/atom"
|
||||||
"github.com/rwinkhart/backtone/bthtml"
|
"github.com/rwinkhart/backtone/websrc"
|
||||||
"github.com/rwinkhart/go-boilerplate/other"
|
"github.com/rwinkhart/go-boilerplate/other"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,11 +33,12 @@ type feedT struct {
|
|||||||
AuthorEmail feedFieldInfoT `xml:"authorEmail"`
|
AuthorEmail feedFieldInfoT `xml:"authorEmail"`
|
||||||
}
|
}
|
||||||
type inputT struct {
|
type inputT struct {
|
||||||
FlareSolverrURL string `xml:"flareSolverrURL"`
|
FlareSolverrEndpoint string `xml:"flareSolverrEndpoint,omitempty"`
|
||||||
Regex string `xml:"regex"`
|
APIMethodEndpointPayload string `xml:"apiMethodEndpointPayload,omitempty"`
|
||||||
LoadSeconds float32 `xml:"loadSeconds"`
|
Regex string `xml:"regex"`
|
||||||
MaxFeedItems int `xml:"maxFeedItems"`
|
LoadSeconds float32 `xml:"loadSeconds"`
|
||||||
Feed feedT `xml:"feed"`
|
MaxFeedItems int `xml:"maxFeedItems"`
|
||||||
|
Feed feedT `xml:"feed"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -44,10 +46,10 @@ func main() {
|
|||||||
if len(os.Args) < 2 {
|
if len(os.Args) < 2 {
|
||||||
demoInputBytes, _ := xml.MarshalIndent(
|
demoInputBytes, _ := xml.MarshalIndent(
|
||||||
inputT{
|
inputT{
|
||||||
FlareSolverrURL: "http://127.0.0.1:8191",
|
FlareSolverrEndpoint: "http://127.0.0.1:8191",
|
||||||
Regex: "PLACEHOLDER",
|
Regex: "PLACEHOLDER",
|
||||||
LoadSeconds: 2.5,
|
LoadSeconds: 2.5,
|
||||||
MaxFeedItems: 10,
|
MaxFeedItems: 10,
|
||||||
Feed: feedT{
|
Feed: feedT{
|
||||||
Title: feedFieldInfoT{Parent: "Demo Title", CaptureGroupIndex: []int8{0}},
|
Title: feedFieldInfoT{Parent: "Demo Title", CaptureGroupIndex: []int8{0}},
|
||||||
Link: feedFieldInfoT{Parent: "https://example.com/", CaptureGroupIndex: []int8{1}},
|
Link: feedFieldInfoT{Parent: "https://example.com/", CaptureGroupIndex: []int8{1}},
|
||||||
@@ -73,9 +75,20 @@ func main() {
|
|||||||
other.PrintError("Failed to unmarshal input XML: "+err.Error(), 1)
|
other.PrintError("Failed to unmarshal input XML: "+err.Error(), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
rawHTML, err := bthtml.GetFromURL(inputInfo.FlareSolverrURL, inputInfo.Feed.Link.Parent, inputInfo.LoadSeconds)
|
var rawString string
|
||||||
if err != nil {
|
if inputInfo.FlareSolverrEndpoint != "" {
|
||||||
other.PrintError("Failed to get HTML content: "+err.Error(), 1)
|
rawString, err = websrc.GetByScrape(inputInfo.FlareSolverrEndpoint, inputInfo.Feed.Link.Parent, inputInfo.LoadSeconds)
|
||||||
|
if err != nil {
|
||||||
|
other.PrintError("Failed to scrape HTML content: "+err.Error(), 1)
|
||||||
|
}
|
||||||
|
} else if inputInfo.APIMethodEndpointPayload != "" {
|
||||||
|
apiInfoSplit := strings.Split(inputInfo.APIMethodEndpointPayload, "@")
|
||||||
|
rawString, err = websrc.GetByAPIJson(apiInfoSplit[0], apiInfoSplit[1], []byte(apiInfoSplit[2]))
|
||||||
|
if err != nil {
|
||||||
|
other.PrintError("Failed to get API response: "+err.Error(), 1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
other.PrintError("Either FlareSolverrEndpoint or APIMethodPayload must be specified", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
atomXML, err := atom.GetFromString(
|
atomXML, err := atom.GetFromString(
|
||||||
@@ -85,7 +98,7 @@ func main() {
|
|||||||
Description: inputInfo.Feed.Description.Parent,
|
Description: inputInfo.Feed.Description.Parent,
|
||||||
Author: &feeds.Author{Name: inputInfo.Feed.AuthorName.Parent, Email: inputInfo.Feed.AuthorEmail.Parent},
|
Author: &feeds.Author{Name: inputInfo.Feed.AuthorName.Parent, Email: inputInfo.Feed.AuthorEmail.Parent},
|
||||||
},
|
},
|
||||||
&rawHTML,
|
&rawString,
|
||||||
inputInfo.Regex,
|
inputInfo.Regex,
|
||||||
atom.IndicesT{
|
atom.IndicesT{
|
||||||
TitleC: inputInfo.Feed.Title.CaptureGroupConnector,
|
TitleC: inputInfo.Feed.Title.CaptureGroupConnector,
|
||||||
@@ -102,7 +115,7 @@ func main() {
|
|||||||
inputInfo.MaxFeedItems,
|
inputInfo.MaxFeedItems,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
other.PrintError("Failed to generate Atom RSS feed from HTML:\n\n"+rawHTML+"\n\n"+err.Error(), 1)
|
other.PrintError("Failed to generate Atom RSS feed from HTML:\n\n"+rawString+"\n\n"+err.Error(), 1)
|
||||||
}
|
}
|
||||||
fmt.Println(*atomXML)
|
fmt.Println(*atomXML)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package websrc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type payloadFlareSolverrT struct {
|
||||||
|
Cmd string `json:"cmd"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
MaxTimeoutMilliseconds int `json:"maxTimeout"`
|
||||||
|
WaitInSeconds float32 `json:"waitInSeconds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetByAPIJson(method, endpoint string, payload []byte) (string, error) {
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
req, _ := http.NewRequest(method, endpoint, bytes.NewBuffer(payload))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("unable to make " + method + " request to \"" + endpoint + "\": " + err.Error())
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("unable to read \"" + endpoint + "\" response body: " + err.Error())
|
||||||
|
}
|
||||||
|
return string(body), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package websrc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetByScrape(flareSolverrEndpoint, webPageURL string, loadSeconds float32) (string, error) {
|
||||||
|
payload, err := json.Marshal(payloadFlareSolverrT{
|
||||||
|
Cmd: "request.get",
|
||||||
|
URL: webPageURL,
|
||||||
|
MaxTimeoutMilliseconds: 60000,
|
||||||
|
WaitInSeconds: loadSeconds,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("unable to marshal FlareSolverr payload to JSON: " + err.Error())
|
||||||
|
}
|
||||||
|
body, err := GetByAPIJson("POST", flareSolverrEndpoint, payload)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(body), nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user