mirror of
https://github.com/by-jp/www.byjp.me.git
synced 2025-08-08 17:15:56 +01:00
Prepare for Instragm posts
This commit is contained in:
parent
9adafb2130
commit
791fd82f64
7 changed files with 378 additions and 2 deletions
54
layouts/instagram-posts/single.html
Normal file
54
layouts/instagram-posts/single.html
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
{{ define "main" }}
|
||||||
|
<main class="post">
|
||||||
|
<article>
|
||||||
|
<div class="post-content">
|
||||||
|
{{ range .Params.Media }}
|
||||||
|
<figure><img src="{{ . }}" /></figure>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ .Content }}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div class="post-info">
|
||||||
|
<p>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather"><rect x="2" y="2" width="20" height="20" rx="5" ry="5"></rect><path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"></path><line x1="17.5" y1="6.5" x2="17.5" y2="6.5"></line></svg>
|
||||||
|
From <a href="../">Instagram archive</a>
|
||||||
|
</p>
|
||||||
|
{{ partial "tags.html" .Params.tags }}
|
||||||
|
{{ partial "categories.html" . }}
|
||||||
|
|
||||||
|
{{- if .GitInfo }}
|
||||||
|
<p><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-git-commit"><circle cx="12" cy="12" r="4"></circle><line x1="1.05" y1="12" x2="7" y2="12"></line><line x1="17.01" y1="12" x2="22.96" y2="12"></line></svg><a href="{{ .Site.Params.gitUrl -}}{{ .GitInfo.Hash }}" target="_blank" rel="noopener">{{ .GitInfo.AbbreviatedHash }}</a> @ {{ if .Site.Params.dateformNum }}{{ dateFormat .Site.Params.dateformNum .GitInfo.AuthorDate.Local }}{{ else }}{{ dateFormat "2006-01-02" .GitInfo.AuthorDate.Local }}{{ end }}</p>
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-calendar">
|
||||||
|
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
|
||||||
|
<line x1="16" y1="2" x2="16" y2="6"></line>
|
||||||
|
<line x1="8" y1="2" x2="8" y2="6"></line>
|
||||||
|
<line x1="3" y1="10" x2="21" y2="10"></line>
|
||||||
|
</svg>
|
||||||
|
{{ if .Site.Params.dateformNumTime }}
|
||||||
|
{{ dateFormat .Site.Params.dateformNumTime .Date.Local }}
|
||||||
|
{{ else }}
|
||||||
|
{{ dateFormat "2006-01-02 15:04" .Date.Local }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ if .Lastmod }}
|
||||||
|
{{ if not (eq .Lastmod .Date )}}
|
||||||
|
{{ if .Site.Params.dateformNumTime }}
|
||||||
|
({{ i18n "lastModified" }}: {{ dateFormat .Site.Params.dateformNumTime .Lastmod.Local }})
|
||||||
|
{{ else }}
|
||||||
|
({{ i18n "lastModified" }}: {{ dateFormat "2006-01-02 15:04" .Lastmod.Local }})
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{{ partial "pagination-single.html" . }}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{{ end }}
|
|
@ -2,7 +2,7 @@
|
||||||
{{ $paginator := .Paginate .Data.Pages }}
|
{{ $paginator := .Paginate .Data.Pages }}
|
||||||
|
|
||||||
<main class="posts">
|
<main class="posts">
|
||||||
<h1>{{ .Title }}</h1>
|
<h1>{{ title (replace .Title "-" " ") }}</h1>
|
||||||
|
|
||||||
{{ if .Content }}
|
{{ if .Content }}
|
||||||
<div class="content">{{ .Content }}</div>
|
<div class="content">{{ .Content }}</div>
|
||||||
|
|
|
@ -41,7 +41,6 @@
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<div class="post-info">
|
<div class="post-info">
|
||||||
{{ .Params.tags }}
|
|
||||||
{{ partial "tags.html" .Params.tags }}
|
{{ partial "tags.html" .Params.tags }}
|
||||||
{{ partial "categories.html" . }}
|
{{ partial "categories.html" . }}
|
||||||
|
|
||||||
|
|
9
tools/archive/instagram/README.md
Normal file
9
tools/archive/instagram/README.md
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# Instagram archive
|
||||||
|
|
||||||
|
This tool will take an [Instagram data archive](https://help.instagram.com/181231772500920) and turn it into a series of timestamped posts for your Hugo blog.
|
||||||
|
|
||||||
|
```
|
||||||
|
go run . <path/to/archive/username_yyyymmdd.zip> <path/to/hugo/root/>
|
||||||
|
```
|
||||||
|
|
||||||
|
It will create one folder per post in the `content/instagram-posts` directory. You can change how those posts look by creating a `layouts/instgram-posts/single.html` template.
|
8
tools/archive/instagram/go.mod
Normal file
8
tools/archive/instagram/go.mod
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
module github.com/jphastings/www.byjp.me/tools/archive/instagram
|
||||||
|
|
||||||
|
go 1.20
|
||||||
|
|
||||||
|
require (
|
||||||
|
golang.org/x/text v0.9.0
|
||||||
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
|
)
|
6
tools/archive/instagram/go.sum
Normal file
6
tools/archive/instagram/go.sum
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||||
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
300
tools/archive/instagram/main.go
Normal file
300
tools/archive/instagram/main.go
Normal file
|
@ -0,0 +1,300 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/base32"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/text/encoding/charmap"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const titleLength = 48
|
||||||
|
const minLength = 16
|
||||||
|
|
||||||
|
var tagMap = map[string]string{
|
||||||
|
"@andyrobert1729": "Andy",
|
||||||
|
"@artphilm": "Phil",
|
||||||
|
"@beckyfuzzymuzzy": "Becky",
|
||||||
|
"@bickertonjane": "Auntie Jane",
|
||||||
|
"@bratpack_ldn": "The Brat Pack",
|
||||||
|
"@buckettafloat": "Erica",
|
||||||
|
"@buckettsails": "Joe",
|
||||||
|
"@chrismhs": "Chris",
|
||||||
|
"@dxcompton": "Dave",
|
||||||
|
"@ericabuckett8": "Erica",
|
||||||
|
"@esther_dr": "Esther",
|
||||||
|
"@hazanj99": "Jenny",
|
||||||
|
"@hazanjon": "Jon",
|
||||||
|
"@helenhs16": "Mum",
|
||||||
|
"@joostsposts": "Joost",
|
||||||
|
"@kaphleenmurthy": "Kathleen",
|
||||||
|
"@le_boyd": "Leanne",
|
||||||
|
"@lydiadr": "Lydia",
|
||||||
|
"@Mr_Bingo": "Mr. Bingo",
|
||||||
|
"@ponkalulu": "Caitlin",
|
||||||
|
"@rosalysbryan": "Rose",
|
||||||
|
"@spagbol_terol": "Paul",
|
||||||
|
"@yvetteedrei": "Yvette",
|
||||||
|
}
|
||||||
|
|
||||||
|
func check(err error, msg string) {
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s\n %v", msg, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type closer interface {
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func doClose(c closer, msg string) {
|
||||||
|
check(c.Close(), msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
type location struct {
|
||||||
|
Name string
|
||||||
|
Latitude float64
|
||||||
|
Longitude float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type frontMatter struct {
|
||||||
|
Title string
|
||||||
|
Media []string
|
||||||
|
Date string
|
||||||
|
Draft bool
|
||||||
|
Tags []string
|
||||||
|
Locations []location
|
||||||
|
}
|
||||||
|
|
||||||
|
type post struct {
|
||||||
|
Media []media
|
||||||
|
Title string
|
||||||
|
CreationTimestamp int64 `json:"creation_timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type media struct {
|
||||||
|
URI string
|
||||||
|
CreationTimestamp int64 `json:"creation_timestamp"`
|
||||||
|
Title string
|
||||||
|
MediaMetadata map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) != 3 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Usage: %s <path/to/instagram/username_yyyymmdd.zip> <path/to/hugo>\n", os.Args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
archive := os.Args[1]
|
||||||
|
hugo := os.Args[2]
|
||||||
|
outputDir := path.Join(hugo, "content", "instagram-posts")
|
||||||
|
|
||||||
|
zf, err := zip.OpenReader(archive)
|
||||||
|
check(err, "Unable to open instagram archive")
|
||||||
|
defer doClose(zf, "Unable to close zipfile")
|
||||||
|
|
||||||
|
postCount, mediaMap, err := createPosts(zf, outputDir)
|
||||||
|
check(err, "Unable to create hugo posts for your instagram data")
|
||||||
|
// TODO: Rewind zip?
|
||||||
|
check(copyMedia(zf, mediaMap), "Unable to copy media to your hugo blog")
|
||||||
|
|
||||||
|
fmt.Printf("Success! %d Instagram posts (with %d images and videos) were added to your hugo blog.\n", postCount, len(mediaMap))
|
||||||
|
}
|
||||||
|
|
||||||
|
var postsFile = regexp.MustCompile(`\Acontent/posts_(\d+)\.json\z`)
|
||||||
|
|
||||||
|
func createPosts(zf *zip.ReadCloser, outputDir string) (int, map[string]string, error) {
|
||||||
|
for _, f := range zf.File {
|
||||||
|
match := postsFile.FindStringSubmatch(f.Name)
|
||||||
|
if len(match) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
jf, err := f.Open()
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
defer doClose(jf, "Unable to close posts file within archive")
|
||||||
|
|
||||||
|
return postsFromFile(jf, outputDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, nil, errors.New("no content/posts_1.json file found in zip file")
|
||||||
|
}
|
||||||
|
|
||||||
|
func postsFromFile(r io.Reader, outputDir string) (int, map[string]string, error) {
|
||||||
|
postsCount := 0
|
||||||
|
mediaMap := make(map[string]string)
|
||||||
|
dec := json.NewDecoder(r)
|
||||||
|
|
||||||
|
// Opening [
|
||||||
|
tok, err := dec.Token()
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
if fmt.Sprintf("%s", tok) != "[" {
|
||||||
|
fmt.Println(tok)
|
||||||
|
return 0, nil, errors.New("posts JSON doesn't start with '['")
|
||||||
|
}
|
||||||
|
|
||||||
|
for dec.More() {
|
||||||
|
var p post
|
||||||
|
if err := dec.Decode(&p); err != nil {
|
||||||
|
return postsCount, mediaMap, errors.New("unable to decode JSON")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := postToPost(p, mediaMap, outputDir); err != nil {
|
||||||
|
return postsCount, mediaMap, err
|
||||||
|
}
|
||||||
|
postsCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
return postsCount, mediaMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var hashtag = regexp.MustCompile(`[#@]\w+`)
|
||||||
|
|
||||||
|
func postToPost(p post, mediaMap map[string]string, outputDir string) error {
|
||||||
|
id, err := postHash(p)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
postDir := path.Join(outputDir, id)
|
||||||
|
|
||||||
|
if err := os.MkdirAll(postDir, 0750); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
text := p.Title
|
||||||
|
if text == "" {
|
||||||
|
text = p.Media[0].Title
|
||||||
|
}
|
||||||
|
if text, err = fixEncoding(text); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fm := frontMatter{}
|
||||||
|
|
||||||
|
fm.Title = text
|
||||||
|
if len(fm.Title) > titleLength {
|
||||||
|
newEnd := minLength
|
||||||
|
for i := titleLength; i > minLength; i-- {
|
||||||
|
if fm.Title[i] == ' ' {
|
||||||
|
newEnd = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fm.Title = fm.Title[:newEnd] + "…"
|
||||||
|
|
||||||
|
if idx := strings.Index(fm.Title, "\n"); idx != -1 {
|
||||||
|
fm.Title = fm.Title[:idx]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
text = escapeMarkdown(text)
|
||||||
|
|
||||||
|
// Add hashtags
|
||||||
|
text = hashtag.ReplaceAllStringFunc(text, func(s string) string {
|
||||||
|
if name, ok := tagMap[s]; ok {
|
||||||
|
tag := strings.ToLower(name)
|
||||||
|
fm.Tags = append(fm.Tags, tag)
|
||||||
|
return fmt.Sprintf("[%s](/tags/%s)", name, tag)
|
||||||
|
} else if s[:1] == "@" {
|
||||||
|
fmt.Println(s)
|
||||||
|
return fmt.Sprintf("[%s](https://instagram.com/%s)", s, s[1:])
|
||||||
|
} else {
|
||||||
|
fm.Tags = append(fm.Tags, s[1:])
|
||||||
|
return fmt.Sprintf("[%s](/tags/%s)", s, s[1:])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add media
|
||||||
|
for i, m := range p.Media {
|
||||||
|
ext := path.Ext(m.URI)
|
||||||
|
if ext == "" {
|
||||||
|
// Some video files have their extensions omitted
|
||||||
|
ext = ".mp4"
|
||||||
|
}
|
||||||
|
|
||||||
|
hugoName := fmt.Sprintf("media-%d%s", i, ext)
|
||||||
|
hugoMedia := path.Join(postDir, hugoName)
|
||||||
|
|
||||||
|
fm.Media = append(fm.Media, hugoName)
|
||||||
|
mediaMap[m.URI] = hugoMedia
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add date
|
||||||
|
publishedAt := time.Unix(p.Media[0].CreationTimestamp, 0).UTC()
|
||||||
|
fm.Date = publishedAt.Format(time.RFC3339)
|
||||||
|
|
||||||
|
// Create post
|
||||||
|
hugoPost, err := os.Create(path.Join(postDir, "index.md"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintln(hugoPost, "---")
|
||||||
|
|
||||||
|
if err := yaml.NewEncoder(hugoPost).Encode(fm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintln(hugoPost, "---")
|
||||||
|
fmt.Fprintln(hugoPost, text)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var fixEncoding = charmap.ISO8859_1.NewEncoder().String
|
||||||
|
|
||||||
|
var markdownEscapable = regexp.MustCompile(`([!\[\]\(\)])`)
|
||||||
|
|
||||||
|
func escapeMarkdown(str string) string {
|
||||||
|
text := strings.ReplaceAll(str, "\n", "\n\n")
|
||||||
|
return markdownEscapable.ReplaceAllString(text, `\$1`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func postHash(p post) (string, error) {
|
||||||
|
h := md5.New()
|
||||||
|
enc := json.NewEncoder(h)
|
||||||
|
if err := enc.Encode(p); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
b64 := base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(h.Sum(nil))
|
||||||
|
return strings.ToLower(b64), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyMedia(zf *zip.ReadCloser, mediaMap map[string]string) error {
|
||||||
|
for _, f := range zf.File {
|
||||||
|
dst, ok := mediaMap[f.Name]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
mf, err := f.Open()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer doClose(mf, "Unable to close media file within archive")
|
||||||
|
|
||||||
|
mediaFile, err := os.Create(dst)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer doClose(mediaFile, "Unable to close media file in blog archive")
|
||||||
|
|
||||||
|
io.Copy(mediaFile, mf)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in a new issue