diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..ab4ee34 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,29 @@ +on: + release: + types: [created] + +jobs: + releases-matrix: + name: Release Go Binary + runs-on: ubuntu-latest + strategy: + matrix: + # build and publish in parallel: linux/386, linux/amd64, linux/arm64, windows/386, windows/amd64, darwin/amd64, darwin/arm64 + goos: [linux, windows, darwin] + goarch: ["386", amd64, arm64] + exclude: + - goarch: "386" + goos: darwin + - goarch: arm64 + goos: windows + steps: + - uses: actions/checkout@v3 + - uses: wangyoucao577/go-release-action@v1.28 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + goos: ${{ matrix.goos }} + goarch: ${{ matrix.goarch }} + goversion: "https://go.dev/dl/go1.18.2.linux-amd64.tar.gz" + project_path: "./cmd/opengraph" + binary_name: "opengraph" + extra_files: LICENSE README.md \ No newline at end of file diff --git a/README.md b/README.md index f51a1a4..bd2a79e 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,31 @@ To download and install this package run: *NOTE: if you need to grab as much info from a page as possible consider using [dyatlov/go-htmlinfo](https://github.com/dyatlov/go-htmlinfo)* -Methods: +## Command line tool + +You can also use `opengraph` from CLI. +You can download latest version of `opengraph` for your OS from [Releases](https://github.com/dyatlov/go-opengraph/releases). + +You can query website endpoints using the tool directly or use it with other tools for your own workflows. + +Example usages: + +```bash +# download and parse html page +./opengraph https://www.youtube.com/watch?v=yhoI42bdwU4 +``` + +```bash +# parse piped html +curl https://www.youtube.com/watch?v=yhoI42bdwU4 | ./opengraph +``` + +```bash +# get video image +./opengraph https://www.youtube.com/watch?v=yhoI42bdwU4 | jq '.images[0].url' +``` + +## Package Methods * `NewOpenGraph()` - create a new OpenGraph instance * `ProcessHTML(buffer io.Reader) error` - process given html into underlying data structure diff --git a/cmd/opengraph/go.mod b/cmd/opengraph/go.mod new file mode 100644 index 0000000..480bc3b --- /dev/null +++ b/cmd/opengraph/go.mod @@ -0,0 +1,8 @@ +module opengraph + +go 1.7 + +require ( + github.com/dyatlov/go-opengraph v0.0.0-20210112100619-dae8665a5b09 + golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 // indirect +) diff --git a/cmd/opengraph/go.sum b/cmd/opengraph/go.sum new file mode 100644 index 0000000..2cbd201 --- /dev/null +++ b/cmd/opengraph/go.sum @@ -0,0 +1,9 @@ +github.com/dyatlov/go-opengraph v0.0.0-20210112100619-dae8665a5b09 h1:AQLr//nh20BzN3hIWj2+/Gt3FwSs8Nwo/nz4hMIcLPg= +github.com/dyatlov/go-opengraph v0.0.0-20210112100619-dae8665a5b09/go.mod h1:nYia/MIs9OyvXXYboPmNOj0gVWo97Wx0sde+ZuKkoM4= +golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 h1:NWy5+hlRbC7HK+PmcXVUmW1IMyFce7to56IUvhUFm7Y= +golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/cmd/opengraph/main.go b/cmd/opengraph/main.go new file mode 100644 index 0000000..a5c2e3b --- /dev/null +++ b/cmd/opengraph/main.go @@ -0,0 +1,65 @@ +// This is a simple program utilising dyatlov/go-opengraph library. +// It outputs Open Graph data either by downloading and parsing html +// or by reading html directly from a pipe +// +// Examples: +// +// Download and parse html page: +// ./opengraph https://www.youtube.com/watch?v=yhoI42bdwU4 +// +// Parse piped html +// curl https://www.youtube.com/watch?v=yhoI42bdwU4 | ./opengraph +// + +package main + +import ( + "bufio" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + + "github.com/dyatlov/go-opengraph/opengraph" +) + +func printHelp() { + fmt.Printf("Usage: %s \n", os.Args[0]) + os.Exit(0) +} + +func main() { + var reader io.Reader + + if len(os.Args) == 2 { + url := os.Args[1] + resp, err := http.Get(url) + if err != nil { + log.Fatalf("Error while fetching url %s: %s", url, err) + } + + reader = resp.Body + + defer resp.Body.Close() + } else if len(os.Args) == 1 { + fi, _ := os.Stdin.Stat() + if (fi.Mode() & os.ModeCharDevice) == 0 { + // pipe + reader = bufio.NewReader(os.Stdin) + } else { + printHelp() + } + } else { + printHelp() + } + + og := opengraph.NewOpenGraph() + if err := og.ProcessHTML(reader); err != nil { + log.Fatalf("Error processing html: %s", err) + } + + output, _ := json.MarshalIndent(og, "", " ") + fmt.Println(string(output)) +} diff --git a/cmd/opengraph/opengraph b/cmd/opengraph/opengraph new file mode 100755 index 0000000..08f0ea7 Binary files /dev/null and b/cmd/opengraph/opengraph differ diff --git a/examples/simple.go b/examples/simple.go index fa128cd..ffd894d 100644 --- a/examples/simple.go +++ b/examples/simple.go @@ -9,8 +9,8 @@ import ( func main() { html := ` - - ` + + ` og := opengraph.NewOpenGraph() err := og.ProcessHTML(strings.NewReader(html)) diff --git a/opengraph/go.mod b/opengraph/go.mod new file mode 100644 index 0000000..4904d6c --- /dev/null +++ b/opengraph/go.mod @@ -0,0 +1,5 @@ +module github.com/dyatlov/go-opengraph/opengraph + +go 1.7 + +require golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 diff --git a/opengraph/go.sum b/opengraph/go.sum new file mode 100644 index 0000000..398c599 --- /dev/null +++ b/opengraph/go.sum @@ -0,0 +1,7 @@ +golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 h1:NWy5+hlRbC7HK+PmcXVUmW1IMyFce7to56IUvhUFm7Y= +golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/opengraph/opengraph.go b/opengraph/opengraph.go index fa18673..089d670 100644 --- a/opengraph/opengraph.go +++ b/opengraph/opengraph.go @@ -8,81 +8,37 @@ import ( "golang.org/x/net/html" "golang.org/x/net/html/atom" -) - -// Image defines Open Graph Image type -type Image struct { - URL string `json:"url"` - SecureURL string `json:"secure_url"` - Type string `json:"type"` - Width uint64 `json:"width"` - Height uint64 `json:"height"` - draft bool `json:"-"` -} - -// Video defines Open Graph Video type -type Video struct { - URL string `json:"url"` - SecureURL string `json:"secure_url"` - Type string `json:"type"` - Width uint64 `json:"width"` - Height uint64 `json:"height"` - draft bool `json:"-"` -} - -// Audio defines Open Graph Audio Type -type Audio struct { - URL string `json:"url"` - SecureURL string `json:"secure_url"` - Type string `json:"type"` - draft bool `json:"-"` -} - -// Article contain Open Graph Article structure -type Article struct { - PublishedTime *time.Time `json:"published_time"` - ModifiedTime *time.Time `json:"modified_time"` - ExpirationTime *time.Time `json:"expiration_time"` - Section string `json:"section"` - Tags []string `json:"tags"` - Authors []*Profile `json:"authors"` -} - -// Profile contains Open Graph Profile structure -type Profile struct { - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Username string `json:"username"` - Gender string `json:"gender"` -} -// Book contains Open Graph Book structure -type Book struct { - ISBN string `json:"isbn"` - ReleaseDate *time.Time `json:"release_date"` - Tags []string `json:"tags"` - Authors []*Profile `json:"authors"` -} + "github.com/dyatlov/go-opengraph/opengraph/types/actor" + "github.com/dyatlov/go-opengraph/opengraph/types/article" + "github.com/dyatlov/go-opengraph/opengraph/types/audio" + "github.com/dyatlov/go-opengraph/opengraph/types/book" + "github.com/dyatlov/go-opengraph/opengraph/types/image" + "github.com/dyatlov/go-opengraph/opengraph/types/music" + "github.com/dyatlov/go-opengraph/opengraph/types/profile" + "github.com/dyatlov/go-opengraph/opengraph/types/video" +) // OpenGraph contains facebook og data type OpenGraph struct { isArticle bool isBook bool isProfile bool - Type string `json:"type"` - URL string `json:"url"` - Title string `json:"title"` - Description string `json:"description"` - Determiner string `json:"determiner"` - SiteName string `json:"site_name"` - Locale string `json:"locale"` - LocalesAlternate []string `json:"locales_alternate"` - Images []*Image `json:"images"` - Audios []*Audio `json:"audios"` - Videos []*Video `json:"videos"` - Article *Article `json:"article,omitempty"` - Book *Book `json:"book,omitempty"` - Profile *Profile `json:"profile,omitempty"` + Type string `json:"type"` + URL string `json:"url"` + Title string `json:"title"` + Description string `json:"description"` + Determiner string `json:"determiner"` + SiteName string `json:"site_name"` + Locale string `json:"locale"` + LocalesAlternate []string `json:"locales_alternate"` + Images []*image.Image `json:"images"` + Audios []*audio.Audio `json:"audios"` + Videos []*video.Video `json:"videos"` + Article *article.Article `json:"article,omitempty"` + Book *book.Book `json:"book,omitempty"` + Profile *profile.Profile `json:"profile,omitempty"` + Music *music.Music `json:"music,omitempty"` } // NewOpenGraph returns new instance of Open Graph structure @@ -137,21 +93,13 @@ func (og *OpenGraph) ensureHasVideo() { if len(og.Videos) > 0 { return } - og.Videos = append(og.Videos, &Video{draft: true}) -} - -func (og *OpenGraph) ensureHasImage() { - if len(og.Images) > 0 { - return - } - og.Images = append(og.Images, &Image{draft: true}) + og.Videos = append(og.Videos, video.NewVideo()) } -func (og *OpenGraph) ensureHasAudio() { - if len(og.Audios) > 0 { - return +func (og *OpenGraph) ensureHasMusic() { + if og.Music == nil { + og.Music = music.NewMusic() } - og.Audios = append(og.Audios, &Audio{draft: true}) } // ProcessMeta processes meta attributes and adds them to Open Graph structure if they are suitable for that @@ -182,73 +130,110 @@ func (og *OpenGraph) ProcessMeta(metaAttrs map[string]string) { case "og:locale:alternate": og.LocalesAlternate = append(og.LocalesAlternate, metaAttrs["content"]) case "og:audio": - if len(og.Audios)>0 && og.Audios[len(og.Audios)-1].draft { - og.Audios[len(og.Audios)-1].URL = metaAttrs["content"] - og.Audios[len(og.Audios)-1].draft = false - } else { - og.Audios = append(og.Audios, &Audio{URL: metaAttrs["content"]}) - } + og.Audios = audio.AddUrl(og.Audios, metaAttrs["content"]) case "og:audio:secure_url": - og.ensureHasAudio() - og.Audios[len(og.Audios)-1].SecureURL = metaAttrs["content"] + og.Audios = audio.AddSecureUrl(og.Audios, metaAttrs["content"]) case "og:audio:type": - og.ensureHasAudio() - og.Audios[len(og.Audios)-1].Type = metaAttrs["content"] + og.Audios = audio.AddType(og.Audios, metaAttrs["content"]) case "og:image": - if len(og.Images)>0 && og.Images[len(og.Images)-1].draft { - og.Images[len(og.Images)-1].URL = metaAttrs["content"] - og.Images[len(og.Images)-1].draft = false - } else { - og.Images = append(og.Images, &Image{URL: metaAttrs["content"]}) - } + og.Images = image.AddURL(og.Images, metaAttrs["content"]) case "og:image:url": - og.ensureHasImage() - og.Images[len(og.Images)-1].URL = metaAttrs["content"] + og.Images = image.AddURL(og.Images, metaAttrs["content"]) case "og:image:secure_url": - og.ensureHasImage() - og.Images[len(og.Images)-1].SecureURL = metaAttrs["content"] + og.Images = image.AddSecureURL(og.Images, metaAttrs["content"]) case "og:image:type": - og.ensureHasImage() - og.Images[len(og.Images)-1].Type = metaAttrs["content"] + og.Images = image.AddType(og.Images, metaAttrs["content"]) case "og:image:width": w, err := strconv.ParseUint(metaAttrs["content"], 10, 64) if err == nil { - og.ensureHasImage() - og.Images[len(og.Images)-1].Width = w + og.Images = image.AddWidth(og.Images, w) } case "og:image:height": h, err := strconv.ParseUint(metaAttrs["content"], 10, 64) if err == nil { - og.ensureHasImage() - og.Images[len(og.Images)-1].Height = h + og.Images = image.AddHeight(og.Images, h) } case "og:video": - if len(og.Videos)>0 && og.Videos[len(og.Videos)-1].draft { - og.Videos[len(og.Videos)-1].URL = metaAttrs["content"] - og.Videos[len(og.Videos)-1].draft = false - } else { - og.Videos = append(og.Videos, &Video{URL: metaAttrs["content"]}) + og.Videos = video.AddURL(og.Videos, metaAttrs["content"]) + case "og:video:tag": + og.Videos = video.AddTag(og.Videos, metaAttrs["content"]) + case "og:video:duration": + if i, err := strconv.ParseUint(metaAttrs["content"], 10, 64); err == nil { + og.Videos = video.AddDuration(og.Videos, i) + } + case "og:video:release_date": + if t, err := time.Parse(time.RFC3339, metaAttrs["content"]); err == nil { + og.Videos = video.AddReleaseDate(og.Videos, &t) } case "og:video:url": - og.ensureHasVideo() - og.Videos[len(og.Videos)-1].URL = metaAttrs["content"] + og.Videos = video.AddURL(og.Videos, metaAttrs["content"]) case "og:video:secure_url": - og.ensureHasVideo() - og.Videos[len(og.Videos)-1].SecureURL = metaAttrs["content"] + og.Videos = video.AddSecureURL(og.Videos, metaAttrs["content"]) case "og:video:type": - og.ensureHasVideo() - og.Videos[len(og.Videos)-1].Type = metaAttrs["content"] + og.Videos = video.AddTag(og.Videos, metaAttrs["content"]) case "og:video:width": w, err := strconv.ParseUint(metaAttrs["content"], 10, 64) if err == nil { - og.ensureHasVideo() - og.Videos[len(og.Videos)-1].Width = w + og.Videos = video.AddWidth(og.Videos, w) } case "og:video:height": h, err := strconv.ParseUint(metaAttrs["content"], 10, 64) if err == nil { - og.ensureHasVideo() - og.Videos[len(og.Videos)-1].Height = h + og.Videos = video.AddHeight(og.Videos, h) + } + case "og:video:actor": + og.ensureHasVideo() + og.Videos[len(og.Videos)-1].Actors = actor.AddProfile(og.Videos[len(og.Videos)-1].Actors, metaAttrs["content"]) + case "og:video:actor:role": + og.ensureHasVideo() + og.Videos[len(og.Videos)-1].Actors = actor.AddRole(og.Videos[len(og.Videos)-1].Actors, metaAttrs["content"]) + case "og:video:director": + og.ensureHasVideo() + og.Videos[len(og.Videos)-1].Directors = append(og.Videos[len(og.Videos)-1].Directors, metaAttrs["content"]) + case "og:video:writer": + og.ensureHasVideo() + og.Videos[len(og.Videos)-1].Writers = append(og.Videos[len(og.Videos)-1].Writers, metaAttrs["content"]) + case "og:music:duration": + og.ensureHasMusic() + if i, err := strconv.ParseUint(metaAttrs["content"], 10, 64); err == nil { + og.Music.Duration = i + } + case "og:music:release_date": + og.ensureHasMusic() + if t, err := time.Parse(time.RFC3339, metaAttrs["content"]); err == nil { + og.Music.ReleaseDate = &t + } + case "og:music:album": + og.ensureHasMusic() + og.Music.Album.URL = metaAttrs["content"] + case "og:music:album:disc": + og.ensureHasMusic() + if i, err := strconv.ParseUint(metaAttrs["content"], 10, 64); err == nil { + og.Music.Album.Disc = i + } + case "og:music:album:track": + og.ensureHasMusic() + if i, err := strconv.ParseUint(metaAttrs["content"], 10, 64); err == nil { + og.Music.Album.Track = i + } + case "og:music:musician": + og.ensureHasMusic() + og.Music.Musicians = append(og.Music.Musicians, metaAttrs["content"]) + case "og:music:creator": + og.ensureHasMusic() + og.Music.Creators = append(og.Music.Creators, metaAttrs["content"]) + case "og:music:song": + og.ensureHasMusic() + og.Music.AddSongUrl(metaAttrs["content"]) + case "og:music:disc": + og.ensureHasMusic() + if i, err := strconv.ParseUint(metaAttrs["content"], 10, 64); err == nil { + og.Music.AddSongDisc(i) + } + case "og:music:track": + og.ensureHasMusic() + if i, err := strconv.ParseUint(metaAttrs["content"], 10, 64); err == nil { + og.Music.AddSongTrack(i) } default: if og.isArticle { @@ -263,100 +248,64 @@ func (og *OpenGraph) ProcessMeta(metaAttrs map[string]string) { func (og *OpenGraph) processArticleMeta(metaAttrs map[string]string) { if og.Article == nil { - og.Article = &Article{} + og.Article = &article.Article{} } switch metaAttrs["property"] { - case "article:published_time": + case "og:article:published_time": t, err := time.Parse(time.RFC3339, metaAttrs["content"]) if err == nil { og.Article.PublishedTime = &t } - case "article:modified_time": + case "og:article:modified_time": t, err := time.Parse(time.RFC3339, metaAttrs["content"]) if err == nil { og.Article.ModifiedTime = &t } - case "article:expiration_time": + case "og:article:expiration_time": t, err := time.Parse(time.RFC3339, metaAttrs["content"]) if err == nil { og.Article.ExpirationTime = &t } - case "article:section": + case "og:article:section": og.Article.Section = metaAttrs["content"] - case "article:tag": + case "og:article:tag": og.Article.Tags = append(og.Article.Tags, metaAttrs["content"]) - case "article:author:first_name": - if len(og.Article.Authors) == 0 { - og.Article.Authors = append(og.Article.Authors, &Profile{}) - } - og.Article.Authors[len(og.Article.Authors)-1].FirstName = metaAttrs["content"] - case "article:author:last_name": - if len(og.Article.Authors) == 0 { - og.Article.Authors = append(og.Article.Authors, &Profile{}) - } - og.Article.Authors[len(og.Article.Authors)-1].LastName = metaAttrs["content"] - case "article:author:username": - if len(og.Article.Authors) == 0 { - og.Article.Authors = append(og.Article.Authors, &Profile{}) - } - og.Article.Authors[len(og.Article.Authors)-1].Username = metaAttrs["content"] - case "article:author:gender": - if len(og.Article.Authors) == 0 { - og.Article.Authors = append(og.Article.Authors, &Profile{}) - } - og.Article.Authors[len(og.Article.Authors)-1].Gender = metaAttrs["content"] + case "og:article:author": + og.Article.Authors = append(og.Article.Authors, metaAttrs["content"]) } } func (og *OpenGraph) processBookMeta(metaAttrs map[string]string) { if og.Book == nil { - og.Book = &Book{} + og.Book = &book.Book{} } switch metaAttrs["property"] { - case "book:release_date": + case "og:book:release_date": t, err := time.Parse(time.RFC3339, metaAttrs["content"]) if err == nil { og.Book.ReleaseDate = &t } - case "book:isbn": + case "og:book:isbn": og.Book.ISBN = metaAttrs["content"] - case "book:tag": + case "og:book:tag": og.Book.Tags = append(og.Book.Tags, metaAttrs["content"]) - case "book:author:first_name": - if len(og.Book.Authors) == 0 { - og.Book.Authors = append(og.Book.Authors, &Profile{}) - } - og.Book.Authors[len(og.Book.Authors)-1].FirstName = metaAttrs["content"] - case "book:author:last_name": - if len(og.Book.Authors) == 0 { - og.Book.Authors = append(og.Book.Authors, &Profile{}) - } - og.Book.Authors[len(og.Book.Authors)-1].LastName = metaAttrs["content"] - case "book:author:username": - if len(og.Book.Authors) == 0 { - og.Book.Authors = append(og.Book.Authors, &Profile{}) - } - og.Book.Authors[len(og.Book.Authors)-1].Username = metaAttrs["content"] - case "book:author:gender": - if len(og.Book.Authors) == 0 { - og.Book.Authors = append(og.Book.Authors, &Profile{}) - } - og.Book.Authors[len(og.Book.Authors)-1].Gender = metaAttrs["content"] + case "og:book:author": + og.Book.Authors = append(og.Book.Authors, metaAttrs["content"]) } } func (og *OpenGraph) processProfileMeta(metaAttrs map[string]string) { if og.Profile == nil { - og.Profile = &Profile{} + og.Profile = &profile.Profile{} } switch metaAttrs["property"] { - case "profile:first_name": + case "og:profile:first_name": og.Profile.FirstName = metaAttrs["content"] - case "profile:last_name": + case "og:profile:last_name": og.Profile.LastName = metaAttrs["content"] - case "profile:username": + case "og:profile:username": og.Profile.Username = metaAttrs["content"] - case "profile:gender": + case "og:profile:gender": og.Profile.Gender = metaAttrs["content"] } } diff --git a/opengraph/opengraph_test.go b/opengraph/opengraph_test.go index 2583ce3..50ffd70 100644 --- a/opengraph/opengraph_test.go +++ b/opengraph/opengraph_test.go @@ -21,8 +21,8 @@ const html = ` - - + + @@ -235,7 +235,7 @@ func TestOpenGraphProcessMeta(t *testing.T) { t.Error("wrong og:type processing") } - og.ProcessMeta(map[string]string{"property": "book:isbn", "content": "123456"}) + og.ProcessMeta(map[string]string{"property": "og:book:isbn", "content": "123456"}) if og.Book == nil { t.Error("wrong book type processing") @@ -245,21 +245,37 @@ func TestOpenGraphProcessMeta(t *testing.T) { } } - og.ProcessMeta(map[string]string{"property": "article:section", "content": "testsection"}) + og.ProcessMeta(map[string]string{"property": "og:article:section", "content": "testsection"}) if og.Article != nil { t.Error("article processed when it should not be") } - og.ProcessMeta(map[string]string{"property": "book:author:first_name", "content": "John"}) + og.ProcessMeta(map[string]string{"property": "og:book:author", "content": "https://site.com/author/VitaliDeatlov"}) + og.ProcessMeta(map[string]string{"property": "og:book:author", "content": "https://site.com/author/JohnDoe"}) if og.Book != nil { - if len(og.Book.Authors) == 0 { - t.Error("book author was not processed") - } else { - if og.Book.Authors[0].FirstName != "John" { - t.Error("author first name was processed incorrectly") - } + if len(og.Book.Authors) != 2 { + t.Error("Incorrect amount of book authors") } + } else { + t.Error("Book data wasn't processed") + } + + og.ProcessMeta(map[string]string{"property": "og:music:creator", "content": "https://site.com/author/JohnDoe"}) + + if og.Music == nil { + t.Error("Incorrectly processed music creator") + } + + og.ProcessMeta(map[string]string{"property": "og:music:song", "content": "https://site.com/song/1"}) + og.ProcessMeta(map[string]string{"property": "og:music:musician", "content": "https://site.com/author/VitaliDeatlov"}) + og.ProcessMeta(map[string]string{"property": "og:music:song", "content": "https://site.com/song/2"}) + + if len(og.Music.Songs) != 2 { + t.Error("Incorrectly parsed music song urls") + } + if len(og.Music.Musicians) != 1 { + t.Error("Incorrectly parsed song musicians") } } diff --git a/opengraph/types/actor/actor.go b/opengraph/types/actor/actor.go new file mode 100644 index 0000000..ed9b995 --- /dev/null +++ b/opengraph/types/actor/actor.go @@ -0,0 +1,27 @@ +package actor + +// Actor contain Open Graph Actor structure +type Actor struct { + Profile string `json:"profile"` + Role string `json:"role"` +} + +func NewActor() *Actor { + return &Actor{} +} + +func AddProfile(actors []*Actor, v string) []*Actor { + if len(actors) == 0 || actors[len(actors)-1].Profile != "" { + actors = append(actors, &Actor{}) + } + actors[len(actors)-1].Profile = v + return actors +} + +func AddRole(actors []*Actor, v string) []*Actor { + if len(actors) == 0 || actors[len(actors)-1].Role != "" { + actors = append(actors, &Actor{}) + } + actors[len(actors)-1].Role = v + return actors +} diff --git a/opengraph/types/article/article.go b/opengraph/types/article/article.go new file mode 100644 index 0000000..19729bc --- /dev/null +++ b/opengraph/types/article/article.go @@ -0,0 +1,15 @@ +package article + +import ( + "time" +) + +// Article contain Open Graph Article structure +type Article struct { + PublishedTime *time.Time `json:"published_time"` + ModifiedTime *time.Time `json:"modified_time"` + ExpirationTime *time.Time `json:"expiration_time"` + Section string `json:"section"` + Tags []string `json:"tags"` + Authors []string `json:"authors"` +} diff --git a/opengraph/types/audio/audio.go b/opengraph/types/audio/audio.go new file mode 100644 index 0000000..094d5a2 --- /dev/null +++ b/opengraph/types/audio/audio.go @@ -0,0 +1,36 @@ +package audio + +// Audio defines Open Graph Audio Type +type Audio struct { + URL string `json:"url"` + SecureURL string `json:"secure_url"` + Type string `json:"type"` +} + +func NewAudio() *Audio { + return &Audio{} +} + +func AddUrl(audios []*Audio, v string) []*Audio { + if len(audios) == 0 || audios[len(audios)-1].URL != "" { + audios = append(audios, &Audio{}) + } + audios[len(audios)-1].URL = v + return audios +} + +func AddSecureUrl(audios []*Audio, v string) []*Audio { + if len(audios) == 0 || audios[len(audios)-1].SecureURL != "" { + audios = append(audios, &Audio{}) + } + audios[len(audios)-1].SecureURL = v + return audios +} + +func AddType(audios []*Audio, v string) []*Audio { + if len(audios) == 0 || audios[len(audios)-1].Type != "" { + audios = append(audios, &Audio{}) + } + audios[len(audios)-1].Type = v + return audios +} diff --git a/opengraph/types/book/book.go b/opengraph/types/book/book.go new file mode 100644 index 0000000..15145f6 --- /dev/null +++ b/opengraph/types/book/book.go @@ -0,0 +1,13 @@ +package book + +import ( + "time" +) + +// Book contains Open Graph Book structure +type Book struct { + ISBN string `json:"isbn"` + ReleaseDate *time.Time `json:"release_date"` + Tags []string `json:"tags"` + Authors []string `json:"authors"` +} diff --git a/opengraph/types/image/image.go b/opengraph/types/image/image.go new file mode 100644 index 0000000..0689265 --- /dev/null +++ b/opengraph/types/image/image.go @@ -0,0 +1,53 @@ +package image + +// Image defines Open Graph Image type +type Image struct { + URL string `json:"url"` + SecureURL string `json:"secure_url"` + Type string `json:"type"` + Width uint64 `json:"width"` + Height uint64 `json:"height"` +} + +func NewImage() *Image { + return &Image{} +} + +func ensureHasImage(images []*Image) []*Image { + if len(images) == 0 { + images = append(images, NewImage()) + } + return images +} + +func AddURL(images []*Image, v string) []*Image { + if len(images) == 0 || (images[len(images)-1].URL != "" && images[len(images)-1].URL != v) { + images = append(images, NewImage()) + } + images[len(images)-1].URL = v + return images +} + +func AddSecureURL(images []*Image, v string) []*Image { + images = ensureHasImage(images) + images[len(images)-1].SecureURL = v + return images +} + +func AddType(images []*Image, v string) []*Image { + images = ensureHasImage(images) + images[len(images)-1].Type = v + return images +} + +func AddWidth(images []*Image, v uint64) []*Image { + images = ensureHasImage(images) + images[len(images)-1].Width = v + return images +} + +func AddHeight(images []*Image, v uint64) []*Image { + images = ensureHasImage(images) + images[len(images)-1].Height = v + return images +} diff --git a/opengraph/types/music/music.go b/opengraph/types/music/music.go new file mode 100644 index 0000000..d214598 --- /dev/null +++ b/opengraph/types/music/music.go @@ -0,0 +1,52 @@ +package music + +import ( + "time" +) + +// Music defines Open Graph Music type +type Music struct { + Musicians []string `json:"musicians,omitempty"` + Creators []string `json:"creators,omitempty"` + Duration uint64 `json:"duration,omitempty"` + ReleaseDate *time.Time `json:"release_date,omitempty"` + Album *Album `json:"album"` + Songs []*Song `json:"songs"` +} + +type Album struct { + URL string `json:"url,omitempty"` + Disc uint64 `json:"disc,omitempty"` + Track uint64 `json:"track,omitempty"` +} + +type Song struct { + URL string `json:"url,omitempty"` + Disc uint64 `json:"disc,omitempty"` + Track uint64 `json:"track,omitempty"` +} + +func NewMusic() *Music { + return &Music{Album: &Album{}} +} + +func (m *Music) AddSongUrl(v string) { + if len(m.Songs) == 0 || m.Songs[len(m.Songs)-1].URL != "" { + m.Songs = append(m.Songs, &Song{}) + } + m.Songs[len(m.Songs)-1].URL = v +} + +func (m *Music) AddSongDisc(v uint64) { + if len(m.Songs) == 0 { + m.Songs = append(m.Songs, &Song{}) + } + m.Songs[len(m.Songs)-1].Disc = v +} + +func (m *Music) AddSongTrack(v uint64) { + if len(m.Songs) == 0 { + m.Songs = append(m.Songs, &Song{}) + } + m.Songs[len(m.Songs)-1].Track = v +} diff --git a/opengraph/types/profile/profile.go b/opengraph/types/profile/profile.go new file mode 100644 index 0000000..f1a78b2 --- /dev/null +++ b/opengraph/types/profile/profile.go @@ -0,0 +1,59 @@ +package profile + +import "strings" + +// Profile contain Open Graph Profile structure +type Profile struct { + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Username string `json:"username"` + Gender string `json:"gender"` +} + +func NewProfile() *Profile { + return &Profile{} +} + +func AddBasicProfile(profiles []*Profile, v string) []*Profile { + parts := strings.SplitN(v, " ", 2) + if len(profiles) == 0 || profiles[len(profiles)-1].FirstName != "" { + profiles = append(profiles, &Profile{}) + } + profiles[len(profiles)-1].FirstName = parts[0] + if len(parts) > 1 { + profiles[len(profiles)-1].LastName = parts[1] + } + return profiles +} + +func AddFirstName(profiles []*Profile, v string) []*Profile { + if len(profiles) == 0 || profiles[len(profiles)-1].FirstName != "" { + profiles = append(profiles, &Profile{}) + } + profiles[len(profiles)-1].FirstName = v + return profiles +} + +func AddLastName(profiles []*Profile, v string) []*Profile { + if len(profiles) == 0 || profiles[len(profiles)-1].LastName != "" { + profiles = append(profiles, &Profile{}) + } + profiles[len(profiles)-1].LastName = v + return profiles +} + +func AddUsername(profiles []*Profile, v string) []*Profile { + if len(profiles) == 0 || profiles[len(profiles)-1].Username != "" { + profiles = append(profiles, &Profile{}) + } + profiles[len(profiles)-1].Username = v + return profiles +} + +func AddGender(profiles []*Profile, v string) []*Profile { + if len(profiles) == 0 || profiles[len(profiles)-1].Gender != "" { + profiles = append(profiles, &Profile{}) + } + profiles[len(profiles)-1].Gender = v + return profiles +} diff --git a/opengraph/types/video/video.go b/opengraph/types/video/video.go new file mode 100644 index 0000000..fc59730 --- /dev/null +++ b/opengraph/types/video/video.go @@ -0,0 +1,83 @@ +package video + +import ( + "time" + + "github.com/dyatlov/go-opengraph/opengraph/types/actor" +) + +// Video defines Open Graph Video type +type Video struct { + URL string `json:"url"` + SecureURL string `json:"secure_url"` + Type string `json:"type"` + Width uint64 `json:"width"` + Height uint64 `json:"height"` + Actors []*actor.Actor `json:"actors,omitempty"` + Directors []string `json:"directors,omitempty"` + Writers []string `json:"writers,omitempty"` + Duration uint64 `json:"duration,omitempty"` + ReleaseDate *time.Time `json:"release_date,omitempty"` + Tags []string `json:"tags,omitempty"` +} + +func NewVideo() *Video { + return &Video{} +} + +func ensureHasVideo(videos []*Video) []*Video { + if len(videos) == 0 { + videos = append(videos, NewVideo()) + } + return videos +} + +func AddURL(videos []*Video, v string) []*Video { + if len(videos) == 0 || (videos[len(videos)-1].URL != "" && videos[len(videos)-1].URL != v) { + videos = append(videos, NewVideo()) + } + videos[len(videos)-1].URL = v + return videos +} + +func AddTag(videos []*Video, v string) []*Video { + videos = ensureHasVideo(videos) + videos[len(videos)-1].Tags = append(videos[len(videos)-1].Tags, v) + return videos +} + +func AddDuration(videos []*Video, v uint64) []*Video { + videos = ensureHasVideo(videos) + videos[len(videos)-1].Duration = v + return videos +} + +func AddReleaseDate(videos []*Video, v *time.Time) []*Video { + videos = ensureHasVideo(videos) + videos[len(videos)-1].ReleaseDate = v + return videos +} + +func AddSecureURL(videos []*Video, v string) []*Video { + videos = ensureHasVideo(videos) + videos[len(videos)-1].SecureURL = v + return videos +} + +func AddType(videos []*Video, v string) []*Video { + videos = ensureHasVideo(videos) + videos[len(videos)-1].Type = v + return videos +} + +func AddWidth(videos []*Video, v uint64) []*Video { + videos = ensureHasVideo(videos) + videos[len(videos)-1].Width = v + return videos +} + +func AddHeight(videos []*Video, v uint64) []*Video { + videos = ensureHasVideo(videos) + videos[len(videos)-1].Height = v + return videos +}