parse.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699
  1. package main
  2. import (
  3. "errors"
  4. "fmt"
  5. "log"
  6. "net/http"
  7. "net/url"
  8. "regexp"
  9. "strconv"
  10. "strings"
  11. "time"
  12. "github.com/ChimeraCoder/anaconda"
  13. "github.com/Jeffail/gabs"
  14. "github.com/PuerkitoBio/goquery"
  15. "golang.org/x/net/html"
  16. "google.golang.org/api/googleapi"
  17. )
  18. const (
  19. imgurClientID = "08af502a9e70d65"
  20. sneakyUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36"
  21. )
  22. var (
  23. twitterClient *anaconda.TwitterApi
  24. )
  25. //#region Twitter
  26. func getTwitterUrls(inputURL string) (map[string]string, error) {
  27. parts := strings.Split(inputURL, ":")
  28. if len(parts) < 2 {
  29. return nil, errors.New("Unable to parse Twitter URL")
  30. }
  31. return map[string]string{"https:" + parts[1] + ":orig": filenameFromURL(parts[1])}, nil
  32. }
  33. func getTwitterStatusUrls(inputURL string, channelID string) (map[string]string, error) {
  34. if twitterClient == nil {
  35. return nil, errors.New("Invalid Twitter API Keys Set")
  36. }
  37. matches := regexUrlTwitterStatus.FindStringSubmatch(inputURL)
  38. statusId, err := strconv.ParseInt(matches[4], 10, 64)
  39. if err != nil {
  40. return nil, err
  41. }
  42. tweet, err := twitterClient.GetTweet(statusId, nil)
  43. if err != nil {
  44. return nil, err
  45. }
  46. links := make(map[string]string)
  47. for _, tweetMedia := range tweet.ExtendedEntities.Media {
  48. if len(tweetMedia.VideoInfo.Variants) > 0 {
  49. var lastVideoVariant anaconda.Variant
  50. for _, videoVariant := range tweetMedia.VideoInfo.Variants {
  51. if videoVariant.Bitrate >= lastVideoVariant.Bitrate {
  52. lastVideoVariant = videoVariant
  53. }
  54. }
  55. if lastVideoVariant.Url != "" {
  56. links[lastVideoVariant.Url] = ""
  57. }
  58. } else {
  59. foundUrls := getDownloadLinks(tweetMedia.Media_url_https, channelID)
  60. for foundUrlKey, foundUrlValue := range foundUrls {
  61. links[foundUrlKey] = foundUrlValue
  62. }
  63. }
  64. }
  65. for _, tweetUrl := range tweet.Entities.Urls {
  66. foundUrls := getDownloadLinks(tweetUrl.Expanded_url, channelID)
  67. for foundUrlKey, foundUrlValue := range foundUrls {
  68. links[foundUrlKey] = foundUrlValue
  69. }
  70. }
  71. return links, nil
  72. }
  73. //#endregion
  74. //#region Instagram
  75. func getInstagramUrls(url string) (map[string]string, error) {
  76. username, shortcode := getInstagramInfo(url)
  77. filename := fmt.Sprintf("instagram %s - %s", username, shortcode)
  78. // if instagram video
  79. videoUrl := getInstagramVideoUrl(url)
  80. if videoUrl != "" {
  81. return map[string]string{videoUrl: filename + filepathExtension(videoUrl)}, nil
  82. }
  83. // if instagram album
  84. albumUrls := getInstagramAlbumUrls(url)
  85. if len(albumUrls) > 0 {
  86. links := make(map[string]string)
  87. for i, albumUrl := range albumUrls {
  88. links[albumUrl] = filename + " " + strconv.Itoa(i+1) + filepathExtension(albumUrl)
  89. }
  90. return links, nil
  91. }
  92. // if instagram picture
  93. afterLastSlash := strings.LastIndex(url, "/")
  94. mediaUrl := url[:afterLastSlash]
  95. mediaUrl += strings.Replace(strings.Replace(url[afterLastSlash:], "?", "&", -1), "/", "/media/?size=l", -1)
  96. return map[string]string{mediaUrl: filename + ".jpg"}, nil
  97. }
  98. func getInstagramInfo(url string) (string, string) {
  99. resp, err := http.Get(url)
  100. if err != nil {
  101. return "unknown", "unknown"
  102. }
  103. defer resp.Body.Close()
  104. z := html.NewTokenizer(resp.Body)
  105. ParseLoop:
  106. for {
  107. tt := z.Next()
  108. switch {
  109. case tt == html.ErrorToken:
  110. break ParseLoop
  111. }
  112. if tt == html.StartTagToken || tt == html.SelfClosingTagToken {
  113. t := z.Token()
  114. for _, a := range t.Attr {
  115. if a.Key == "type" {
  116. if a.Val == "text/javascript" {
  117. z.Next()
  118. content := string(z.Text())
  119. if strings.Contains(content, "window._sharedData = ") {
  120. content = strings.Replace(content, "window._sharedData = ", "", 1)
  121. content = content[:len(content)-1]
  122. jsonParsed, err := gabs.ParseJSON([]byte(content))
  123. if err != nil {
  124. log.Println("Error parsing instagram json:", err)
  125. continue ParseLoop
  126. }
  127. entryChildren, err := jsonParsed.Path("entry_data.PostPage").Children()
  128. if err != nil {
  129. log.Println("Unable to find entries children:", err)
  130. continue ParseLoop
  131. }
  132. for _, entryChild := range entryChildren {
  133. shortcode := entryChild.Path("graphql.shortcode_media.shortcode").Data().(string)
  134. username := entryChild.Path("graphql.shortcode_media.owner.username").Data().(string)
  135. return username, shortcode
  136. }
  137. }
  138. }
  139. }
  140. }
  141. }
  142. }
  143. return "unknown", "unknown"
  144. }
  145. func getInstagramVideoUrl(url string) string {
  146. resp, err := http.Get(url)
  147. if err != nil {
  148. return ""
  149. }
  150. defer resp.Body.Close()
  151. z := html.NewTokenizer(resp.Body)
  152. for {
  153. tt := z.Next()
  154. switch {
  155. case tt == html.ErrorToken:
  156. return ""
  157. }
  158. if tt == html.StartTagToken || tt == html.SelfClosingTagToken {
  159. t := z.Token()
  160. if t.Data == "meta" {
  161. for _, a := range t.Attr {
  162. if a.Key == "property" {
  163. if a.Val == "og:video" || a.Val == "og:video:secure_url" {
  164. for _, at := range t.Attr {
  165. if at.Key == "content" {
  166. return at.Val
  167. }
  168. }
  169. }
  170. }
  171. }
  172. }
  173. }
  174. }
  175. }
  176. func getInstagramAlbumUrls(url string) []string {
  177. var links []string
  178. resp, err := http.Get(url)
  179. if err != nil {
  180. return links
  181. }
  182. defer resp.Body.Close()
  183. z := html.NewTokenizer(resp.Body)
  184. ParseLoop:
  185. for {
  186. tt := z.Next()
  187. switch {
  188. case tt == html.ErrorToken:
  189. break ParseLoop
  190. }
  191. if tt == html.StartTagToken || tt == html.SelfClosingTagToken {
  192. t := z.Token()
  193. for _, a := range t.Attr {
  194. if a.Key == "type" {
  195. if a.Val == "text/javascript" {
  196. z.Next()
  197. content := string(z.Text())
  198. if strings.Contains(content, "window._sharedData = ") {
  199. content = strings.Replace(content, "window._sharedData = ", "", 1)
  200. content = content[:len(content)-1]
  201. jsonParsed, err := gabs.ParseJSON([]byte(content))
  202. if err != nil {
  203. log.Println("Error parsing instagram json: ", err)
  204. continue ParseLoop
  205. }
  206. entryChildren, err := jsonParsed.Path("entry_data.PostPage").Children()
  207. if err != nil {
  208. log.Println("Unable to find entries children: ", err)
  209. continue ParseLoop
  210. }
  211. for _, entryChild := range entryChildren {
  212. albumChildren, err := entryChild.Path("graphql.shortcode_media.edge_sidecar_to_children.edges").Children()
  213. if err != nil {
  214. continue ParseLoop
  215. }
  216. for _, albumChild := range albumChildren {
  217. link, ok := albumChild.Path("node.display_url").Data().(string)
  218. if ok {
  219. links = append(links, link)
  220. }
  221. }
  222. }
  223. }
  224. }
  225. }
  226. }
  227. }
  228. }
  229. if len(links) > 0 {
  230. log.Printf("Found instagram album with %d images (url: %s)\n", len(links), url)
  231. }
  232. return links
  233. }
  234. //#endregion
  235. //#region Imgur
  236. func getImgurSingleUrls(url string) (map[string]string, error) {
  237. url = regexp.MustCompile(`(r\/[^\/]+\/)`).ReplaceAllString(url, "") // remove subreddit url
  238. url = strings.Replace(url, "imgur.com/", "imgur.com/download/", -1)
  239. url = strings.Replace(url, ".gifv", "", -1)
  240. return map[string]string{url: ""}, nil
  241. }
  242. type imgurAlbumObject struct {
  243. Data []struct {
  244. Link string
  245. }
  246. }
  247. func getImgurAlbumUrls(url string) (map[string]string, error) {
  248. url = regexp.MustCompile(`(#[A-Za-z0-9]+)?$`).ReplaceAllString(url, "") // remove anchor
  249. afterLastSlash := strings.LastIndex(url, "/")
  250. albumId := url[afterLastSlash+1:]
  251. headers := make(map[string]string)
  252. headers["Authorization"] = "Client-ID " + imgurClientID
  253. imgurAlbumObject := new(imgurAlbumObject)
  254. getJSONwithHeaders("https://api.imgur.com/3/album/"+albumId+"/images", imgurAlbumObject, headers)
  255. links := make(map[string]string)
  256. for _, v := range imgurAlbumObject.Data {
  257. links[v.Link] = ""
  258. }
  259. if len(links) <= 0 {
  260. return getImgurSingleUrls(url)
  261. }
  262. log.Printf("Found imgur album with %d images (url: %s)\n", len(links), url)
  263. return links, nil
  264. }
  265. //#endregion
  266. //#region Streamable
  267. type streamableObject struct {
  268. Status int `json:"status"`
  269. Title string `json:"title"`
  270. Files struct {
  271. Mp4 struct {
  272. URL string `json:"url"`
  273. Width int `json:"width"`
  274. Height int `json:"height"`
  275. } `json:"mp4"`
  276. Mp4Mobile struct {
  277. URL string `json:"url"`
  278. Width int `json:"width"`
  279. Height int `json:"height"`
  280. } `json:"mp4-mobile"`
  281. } `json:"files"`
  282. URL string `json:"url"`
  283. ThumbnailURL string `json:"thumbnail_url"`
  284. Message interface{} `json:"message"`
  285. }
  286. func getStreamableUrls(url string) (map[string]string, error) {
  287. matches := regexUrlStreamable.FindStringSubmatch(url)
  288. shortcode := matches[3]
  289. if shortcode == "" {
  290. return nil, errors.New("Unable to get shortcode from URL")
  291. }
  292. reqUrl := fmt.Sprintf("https://api.streamable.com/videos/%s", shortcode)
  293. streamable := new(streamableObject)
  294. getJSON(reqUrl, streamable)
  295. if streamable.Status != 2 || streamable.Files.Mp4.URL == "" {
  296. return nil, errors.New("Streamable object has no download candidate")
  297. }
  298. link := streamable.Files.Mp4.URL
  299. if !strings.HasPrefix(link, "http") {
  300. link = "https:" + link
  301. }
  302. links := make(map[string]string)
  303. links[link] = ""
  304. return links, nil
  305. }
  306. //#endregion
  307. //#region Gfycat
  308. type gfycatObject struct {
  309. GfyItem struct {
  310. Mp4URL string `json:"mp4Url"`
  311. } `json:"gfyItem"`
  312. }
  313. func getGfycatUrls(url string) (map[string]string, error) {
  314. parts := strings.Split(url, "/")
  315. if len(parts) < 3 {
  316. return nil, errors.New("Unable to parse Gfycat URL")
  317. }
  318. gfycatId := parts[len(parts)-1]
  319. gfycatObject := new(gfycatObject)
  320. getJSON("https://api.gfycat.com/v1/gfycats/"+gfycatId, gfycatObject)
  321. gfycatUrl := gfycatObject.GfyItem.Mp4URL
  322. if url == "" {
  323. return nil, errors.New("Failed to read response from Gfycat")
  324. }
  325. return map[string]string{gfycatUrl: ""}, nil
  326. }
  327. //#endregion
  328. //#region Flickr
  329. type flickrPhotoSizeObject struct {
  330. Label string `json:"label"`
  331. Width int `json:"width,int,string"`
  332. Height int `json:"height,int,string"`
  333. Source string `json:"source"`
  334. URL string `json:"url"`
  335. Media string `json:"media"`
  336. }
  337. type flickrPhotoObject struct {
  338. Sizes struct {
  339. Canblog int `json:"canblog"`
  340. Canprint int `json:"canprint"`
  341. Candownload int `json:"candownload"`
  342. Size []flickrPhotoSizeObject `json:"size"`
  343. } `json:"sizes"`
  344. Stat string `json:"stat"`
  345. }
  346. func getFlickrUrlFromPhotoId(photoId string) string {
  347. reqUrl := fmt.Sprintf("https://www.flickr.com/services/rest/?format=json&nojsoncallback=1&method=%s&api_key=%s&photo_id=%s",
  348. "flickr.photos.getSizes", config.Credentials.FlickrApiKey, photoId)
  349. flickrPhoto := new(flickrPhotoObject)
  350. getJSON(reqUrl, flickrPhoto)
  351. var bestSize flickrPhotoSizeObject
  352. for _, size := range flickrPhoto.Sizes.Size {
  353. if bestSize.Label == "" {
  354. bestSize = size
  355. } else {
  356. if size.Width > bestSize.Width || size.Height > bestSize.Height {
  357. bestSize = size
  358. }
  359. }
  360. }
  361. return bestSize.Source
  362. }
  363. func getFlickrPhotoUrls(url string) (map[string]string, error) {
  364. if config.Credentials.FlickrApiKey == "" {
  365. return nil, errors.New("Invalid Flickr API Key Set")
  366. }
  367. matches := regexUrlFlickrPhoto.FindStringSubmatch(url)
  368. photoId := matches[5]
  369. if photoId == "" {
  370. return nil, errors.New("Unable to get Photo ID from URL")
  371. }
  372. return map[string]string{getFlickrUrlFromPhotoId(photoId): ""}, nil
  373. }
  374. type flickrAlbumObject struct {
  375. Photoset struct {
  376. ID string `json:"id"`
  377. Primary string `json:"primary"`
  378. Owner string `json:"owner"`
  379. Ownername string `json:"ownername"`
  380. Photo []struct {
  381. ID string `json:"id"`
  382. Secret string `json:"secret"`
  383. Server string `json:"server"`
  384. Farm int `json:"farm"`
  385. Title string `json:"title"`
  386. Isprimary string `json:"isprimary"`
  387. Ispublic int `json:"ispublic"`
  388. Isfriend int `json:"isfriend"`
  389. Isfamily int `json:"isfamily"`
  390. } `json:"photo"`
  391. Page int `json:"page"`
  392. PerPage int `json:"per_page"`
  393. Perpage int `json:"perpage"`
  394. Pages int `json:"pages"`
  395. Total string `json:"total"`
  396. Title string `json:"title"`
  397. } `json:"photoset"`
  398. Stat string `json:"stat"`
  399. }
  400. func getFlickrAlbumUrls(url string) (map[string]string, error) {
  401. if config.Credentials.FlickrApiKey == "" {
  402. return nil, errors.New("Invalid Flickr API Key Set")
  403. }
  404. matches := regexUrlFlickrAlbum.FindStringSubmatch(url)
  405. if len(matches) < 10 || matches[9] == "" {
  406. return nil, errors.New("Unable to find Flickr Album ID in URL")
  407. }
  408. albumId := matches[9]
  409. if albumId == "" {
  410. return nil, errors.New("Unable to get Album ID from URL")
  411. }
  412. reqUrl := fmt.Sprintf("https://www.flickr.com/services/rest/?format=json&nojsoncallback=1&method=%s&api_key=%s&photoset_id=%s&per_page=500",
  413. "flickr.photosets.getPhotos", config.Credentials.FlickrApiKey, albumId)
  414. flickrAlbum := new(flickrAlbumObject)
  415. getJSON(reqUrl, flickrAlbum)
  416. links := make(map[string]string)
  417. for _, photo := range flickrAlbum.Photoset.Photo {
  418. links[getFlickrUrlFromPhotoId(photo.ID)] = ""
  419. }
  420. return links, nil
  421. }
  422. func getFlickrAlbumShortUrls(url string) (map[string]string, error) {
  423. result, err := http.Get(url)
  424. if err != nil {
  425. return nil, errors.New("Error getting long URL from shortened Flickr Album URL: " + err.Error())
  426. }
  427. if regexUrlFlickrAlbum.MatchString(result.Request.URL.String()) {
  428. return getFlickrAlbumUrls(result.Request.URL.String())
  429. }
  430. return nil, errors.New("Encountered invalid URL while trying to get long URL from short Flickr Album URL")
  431. }
  432. //#endregion
  433. //#region Google Drive
  434. func getGoogleDriveUrls(url string) (map[string]string, error) {
  435. parts := strings.Split(url, "/")
  436. if len(parts) != 7 {
  437. return nil, errors.New("unable to parse google drive url")
  438. }
  439. fileId := parts[len(parts)-2]
  440. return map[string]string{"https://drive.google.com/uc?export=download&id=" + fileId: ""}, nil
  441. }
  442. func getGoogleDriveFolderUrls(url string) (map[string]string, error) {
  443. matches := regexUrlGoogleDriveFolder.FindStringSubmatch(url)
  444. if len(matches) < 4 || matches[3] == "" {
  445. return nil, errors.New("unable to find google drive folder ID in link")
  446. }
  447. if googleDriveService.BasePath == "" {
  448. return nil, errors.New("please set up google credentials")
  449. }
  450. googleDriveFolderID := matches[3]
  451. links := make(map[string]string)
  452. driveQuery := fmt.Sprintf("\"%s\" in parents", googleDriveFolderID)
  453. driveFields := "nextPageToken, files(id)"
  454. result, err := googleDriveService.Files.List().Q(driveQuery).Fields(googleapi.Field(driveFields)).PageSize(1000).Do()
  455. if err != nil {
  456. log.Println("driveQuery:", driveQuery)
  457. log.Println("driveFields:", driveFields)
  458. log.Println("err:", err)
  459. return nil, err
  460. }
  461. for _, file := range result.Files {
  462. fileUrl := "https://drive.google.com/uc?export=download&id=" + file.Id
  463. links[fileUrl] = ""
  464. }
  465. for {
  466. if result.NextPageToken == "" {
  467. break
  468. }
  469. result, err = googleDriveService.Files.List().Q(driveQuery).Fields(googleapi.Field(driveFields)).PageSize(1000).PageToken(result.NextPageToken).Do()
  470. if err != nil {
  471. return nil, err
  472. }
  473. for _, file := range result.Files {
  474. links[file.Id] = ""
  475. }
  476. }
  477. return links, nil
  478. }
  479. //#endregion
  480. //#region Tistory
  481. // getTistoryUrls downloads tistory URLs
  482. // http://t1.daumcdn.net/cfile/tistory/[…] => http://t1.daumcdn.net/cfile/tistory/[…]
  483. // http://t1.daumcdn.net/cfile/tistory/[…]?original => as is
  484. func getTistoryUrls(link string) (map[string]string, error) {
  485. if !strings.HasSuffix(link, "?original") {
  486. link += "?original"
  487. }
  488. return map[string]string{link: ""}, nil
  489. }
  490. func getLegacyTistoryUrls(link string) (map[string]string, error) {
  491. link = strings.Replace(link, "/image/", "/original/", -1)
  492. return map[string]string{link: ""}, nil
  493. }
  494. func getTistoryWithCDNUrls(urlI string) (map[string]string, error) {
  495. parameters, _ := url.ParseQuery(urlI)
  496. if val, ok := parameters["fname"]; ok {
  497. if len(val) > 0 {
  498. if regexUrlTistoryLegacy.MatchString(val[0]) {
  499. return getLegacyTistoryUrls(val[0])
  500. }
  501. }
  502. }
  503. return nil, nil
  504. }
  505. func getPossibleTistorySiteUrls(url string) (map[string]string, error) {
  506. client := new(http.Client)
  507. request, err := http.NewRequest("HEAD", url, nil)
  508. if err != nil {
  509. return nil, err
  510. }
  511. request.Header.Add("Accept-Encoding", "identity")
  512. request.Header.Add("User-Agent", sneakyUserAgent)
  513. respHead, err := client.Do(request)
  514. if err != nil {
  515. return nil, err
  516. }
  517. contentType := ""
  518. for headerKey, headerValue := range respHead.Header {
  519. if headerKey == "Content-Type" {
  520. contentType = headerValue[0]
  521. }
  522. }
  523. if !strings.Contains(contentType, "text/html") {
  524. return nil, nil
  525. }
  526. request, err = http.NewRequest("GET", url, nil)
  527. if err != nil {
  528. return nil, err
  529. }
  530. request.Header.Add("Accept-Encoding", "identity")
  531. request.Header.Add("User-Agent", sneakyUserAgent)
  532. resp, err := client.Do(request)
  533. if err != nil {
  534. return nil, err
  535. }
  536. doc, err := goquery.NewDocumentFromResponse(resp)
  537. if err != nil {
  538. return nil, err
  539. }
  540. var links = make(map[string]string)
  541. doc.Find(".article img, #content img, div[role=main] img, .section_blogview img").Each(func(i int, s *goquery.Selection) {
  542. foundUrl, exists := s.Attr("src")
  543. if exists {
  544. isTistoryCdnUrl := regexUrlTistoryLegacyWithCDN.MatchString(foundUrl)
  545. isTistoryUrl := regexUrlTistoryLegacy.MatchString(foundUrl)
  546. if isTistoryCdnUrl == true {
  547. finalTistoryUrls, _ := getTistoryWithCDNUrls(foundUrl)
  548. if len(finalTistoryUrls) > 0 {
  549. for finalTistoryUrl := range finalTistoryUrls {
  550. foundFilename := s.AttrOr("filename", "")
  551. links[finalTistoryUrl] = foundFilename
  552. }
  553. }
  554. } else if isTistoryUrl == true {
  555. finalTistoryUrls, _ := getLegacyTistoryUrls(foundUrl)
  556. if len(finalTistoryUrls) > 0 {
  557. for finalTistoryUrl := range finalTistoryUrls {
  558. foundFilename := s.AttrOr("filename", "")
  559. links[finalTistoryUrl] = foundFilename
  560. }
  561. }
  562. }
  563. }
  564. })
  565. if len(links) > 0 {
  566. log.Printf("[%s] Found tistory album with %d images (url: %s)\n", time.Now().Format(time.Stamp), len(links), url)
  567. }
  568. return links, nil
  569. }
  570. //#endregion
  571. //#region Reddit
  572. // This is very crude but works for now
  573. type redditThreadObject []struct {
  574. Kind string `json:"kind"`
  575. Data struct {
  576. Children interface{} `json:"children"`
  577. } `json:"data"`
  578. }
  579. func getRedditPostUrls(link string) (map[string]string, error) {
  580. redditThread := new(redditThreadObject)
  581. headers := make(map[string]string)
  582. headers["Accept-Encoding"] = "identity"
  583. headers["User-Agent"] = sneakyUserAgent
  584. err := getJSONwithHeaders(link+".json", redditThread, headers)
  585. if err != nil {
  586. return nil, fmt.Errorf("Failed to parse json from reddit post:\t%s", err)
  587. }
  588. redditPost := (*redditThread)[0].Data.Children.([]interface{})[0].(map[string]interface{})
  589. redditPostData := redditPost["data"].(map[string]interface{})
  590. if redditPostData["url_overridden_by_dest"] != nil {
  591. redditLink := redditPostData["url_overridden_by_dest"].(string)
  592. filename := fmt.Sprintf("Reddit-%s_%s %s", redditPostData["subreddit"].(string), redditPostData["id"].(string), filenameFromURL(redditLink))
  593. return map[string]string{redditLink: filename}, nil
  594. }
  595. return nil, nil
  596. }
  597. //#endregion
  598. //#region Mastodon
  599. func getMastodonPostUrls(link string) (map[string]string, error) {
  600. var post map[string]interface{}
  601. err := getJSON(link+".json", &post)
  602. if err != nil {
  603. return nil, fmt.Errorf("Failed to parse json from mastodon post:\t%s", err)
  604. }
  605. // Check for returned error
  606. if errmsg, exists := post["error"]; exists {
  607. return nil, fmt.Errorf("Mastodon JSON returned an error:\t%s", errmsg)
  608. }
  609. // Check validity
  610. if attachments, exists := post["attachment"]; exists {
  611. files := make(map[string]string)
  612. for _, attachmentObj := range attachments.([]interface{}) {
  613. attachment := attachmentObj.(map[string]interface{})
  614. files[attachment["url"].(string)] = ""
  615. }
  616. return files, nil
  617. }
  618. return nil, nil
  619. }
  620. //#endregion