Gregory Rudolph
3 years ago
commit
ac61662d6d
7 changed files with 409 additions and 0 deletions
@ -0,0 +1,33 @@ |
|||||||
|
kind: pipeline |
||||||
|
type: docker |
||||||
|
name: default |
||||||
|
|
||||||
|
steps: |
||||||
|
- name: Build |
||||||
|
image: golang |
||||||
|
commands: |
||||||
|
- go get ./.. |
||||||
|
- go vet -v |
||||||
|
- env |
||||||
|
- GOOS="linux" go build -ldflags="-w -s" -o BackgroundDaemon-Linux |
||||||
|
- GOOS="windows" go build -ldflags="-w -s" -o BackgroundDaemon-Win.exe |
||||||
|
- GOOS="darwin" go build -ldflags="-w -s" -o BackgroundDaemon-macOS |
||||||
|
- name: gitea_release |
||||||
|
image: plugins/gitea-release |
||||||
|
settings: |
||||||
|
base_url: https://git.nightmare.haus |
||||||
|
api_key: |
||||||
|
from_secret: gitea_token |
||||||
|
files: |
||||||
|
- ./BackgroundDaemon-Linux |
||||||
|
- ./BackgroundDaemon-Win.exe |
||||||
|
- ./BackgroundDaemon-macOS |
||||||
|
checksum: |
||||||
|
- md5 |
||||||
|
- sha1 |
||||||
|
- sha256 |
||||||
|
- sha512 |
||||||
|
- adler32 |
||||||
|
- crc32 |
||||||
|
when: |
||||||
|
event: [tag] |
@ -0,0 +1,20 @@ |
|||||||
|
module git.nightmare.haus/rudi/BackgroundDaemon |
||||||
|
|
||||||
|
go 1.17 |
||||||
|
|
||||||
|
require ( |
||||||
|
github.com/fogleman/gg v1.3.0 |
||||||
|
github.com/reujab/wallpaper v0.0.0-20210630195606-5f9f655b3740 |
||||||
|
) |
||||||
|
|
||||||
|
require ( |
||||||
|
github.com/gen2brain/shm v0.0.0-20200228170931-49f9650110c5 // indirect |
||||||
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect |
||||||
|
github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240 // indirect |
||||||
|
github.com/kbinani/screenshot v0.0.0-20210720154843-7d3a670d8329 // indirect |
||||||
|
github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect |
||||||
|
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect |
||||||
|
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13 // indirect |
||||||
|
gopkg.in/ini.v1 v1.62.0 // indirect |
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect |
||||||
|
) |
@ -0,0 +1,40 @@ |
|||||||
|
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= |
||||||
|
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= |
||||||
|
github.com/gen2brain/shm v0.0.0-20200228170931-49f9650110c5 h1:Y5Q2mEwfzjMt5+3u70Gtw93ZOu2UuPeeeTBDntF7FoY= |
||||||
|
github.com/gen2brain/shm v0.0.0-20200228170931-49f9650110c5/go.mod h1:uF6rMu/1nvu+5DpiRLwusA6xB8zlkNoGzKn8lmYONUo= |
||||||
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= |
||||||
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= |
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= |
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= |
||||||
|
github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240 h1:dy+DS31tGEGCsZzB45HmJJNHjur8GDgtRNX9U7HnSX4= |
||||||
|
github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240/go.mod h1:3P4UH/k22rXyHIJD2w4h2XMqPX4Of/eySEZq9L6wqc4= |
||||||
|
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= |
||||||
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= |
||||||
|
github.com/kbinani/screenshot v0.0.0-20210720154843-7d3a670d8329 h1:qq2nCpSrXrmvDGRxW0ruW9BVEV1CN2a9YDOExdt+U0o= |
||||||
|
github.com/kbinani/screenshot v0.0.0-20210720154843-7d3a670d8329/go.mod h1:2VPVQDR4wO7KXHwP+DAypEy67rXf+okUx2zjgpCxZw4= |
||||||
|
github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc= |
||||||
|
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= |
||||||
|
github.com/reujab/wallpaper v0.0.0-20210630195606-5f9f655b3740 h1:X6IDPPN+zrSClp0Q+JiERA//d8L0WcU5MqcGeulCW1A= |
||||||
|
github.com/reujab/wallpaper v0.0.0-20210630195606-5f9f655b3740/go.mod h1:WYwPVmM/8szeItLeWkwZSLRvQgrvsvstRzgznR8+E4Q= |
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= |
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= |
||||||
|
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= |
||||||
|
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= |
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= |
||||||
|
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ= |
||||||
|
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= |
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= |
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= |
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
||||||
|
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13 h1:5jaG59Zhd+8ZXe8C+lgiAGqkOaZBruqrWclLkgAww34= |
||||||
|
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= |
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= |
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= |
||||||
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= |
||||||
|
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/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= |
||||||
|
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= |
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= |
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= |
@ -0,0 +1,187 @@ |
|||||||
|
package main |
||||||
|
|
||||||
|
// imports
|
||||||
|
import ( |
||||||
|
"encoding/json" |
||||||
|
"flag" |
||||||
|
"fmt" |
||||||
|
"image" |
||||||
|
_ "image/jpeg" |
||||||
|
"image/png" |
||||||
|
"io" |
||||||
|
"log" |
||||||
|
"math/rand" |
||||||
|
"net/http" |
||||||
|
"os" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/kbinani/screenshot" |
||||||
|
"github.com/reujab/wallpaper" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
sourceImgName string |
||||||
|
outputImgName string |
||||||
|
useNasa bool |
||||||
|
useRandom bool |
||||||
|
setWallpaper bool |
||||||
|
useSketch bool |
||||||
|
nasaAPI string |
||||||
|
totalCycleCount = 5000 |
||||||
|
) |
||||||
|
|
||||||
|
func init() { |
||||||
|
flag.BoolVar(&useNasa, "nasa", false, "Use NASA Astronomy Pic Of the Day as a source image") |
||||||
|
flag.BoolVar(&setWallpaper, "set", false, "Set user wallpaper to the output.") |
||||||
|
flag.BoolVar(&useRandom, "unsplash", false, "Use random unsplash image for seed (Will grab dimensions from background or source if set)") |
||||||
|
flag.BoolVar(&useSketch, "sketch", false, "Use Sketch to generate a new image.") |
||||||
|
flag.StringVar(&sourceImgName, "source", "", "Source image, will override current background") |
||||||
|
flag.StringVar(&outputImgName, "output", "out.png", "Output image filename") |
||||||
|
flag.StringVar(&nasaAPI, "nasa-key", "DEMO_KEY", "Supply your own API Key for NASA APOD") |
||||||
|
} |
||||||
|
|
||||||
|
func main() { |
||||||
|
flag.Parse() |
||||||
|
background, err := wallpaper.Get() |
||||||
|
if err != nil { |
||||||
|
log.Panicln(err) |
||||||
|
} |
||||||
|
bounds := screenshot.GetDisplayBounds(0) |
||||||
|
|
||||||
|
var img image.Image |
||||||
|
if useRandom { |
||||||
|
img, err = loadRandomUnsplashImage(bounds.Dx(), bounds.Dy()) |
||||||
|
if err != nil { |
||||||
|
log.Panicln(err) |
||||||
|
} |
||||||
|
} else if useNasa { |
||||||
|
resp, err := http.Get(fmt.Sprintf("https://api.nasa.gov/planetary/apod?api_key=%+v", nasaAPI)) |
||||||
|
if err != nil { |
||||||
|
log.Fatalf("\nUnable to get NASA API Response\n") |
||||||
|
return |
||||||
|
} |
||||||
|
var apod APODResponse |
||||||
|
decoder := json.NewDecoder(resp.Body) |
||||||
|
err = decoder.Decode(&apod) |
||||||
|
if err != nil { |
||||||
|
log.Fatalf("\nUnable to decode APOD Response: \n%+v\n", resp) |
||||||
|
return |
||||||
|
} |
||||||
|
file, err := os.CreateTemp(".", apod.Hdurl) |
||||||
|
if err != nil { |
||||||
|
log.Panicln(err) |
||||||
|
} |
||||||
|
defer os.Remove(file.Name()) |
||||||
|
downloadFile(file.Name(), apod.Hdurl) |
||||||
|
img, err = loadImage(file.Name()) |
||||||
|
if err != nil { |
||||||
|
log.Panicln(err) |
||||||
|
} |
||||||
|
} else if len(sourceImgName) > 0 { |
||||||
|
img, err = loadImage(sourceImgName) |
||||||
|
if err != nil { |
||||||
|
log.Panicln(err) |
||||||
|
} |
||||||
|
} else { |
||||||
|
img, err = loadImage(background) |
||||||
|
if err != nil { |
||||||
|
log.Println(background) |
||||||
|
log.Panicln(err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
s := NewSketch(img, UserParams{ |
||||||
|
StrokeRatio: 0.75, |
||||||
|
DestWidth: bounds.Dx(), |
||||||
|
DestHeight: bounds.Dy(), |
||||||
|
InitialAlpha: 0.1, |
||||||
|
StrokeReduction: 0.002, |
||||||
|
AlphaIncrease: 0.06, |
||||||
|
StrokeInversionThreshold: 0.05, |
||||||
|
StrokeJitter: int(0.1 * float64(bounds.Dx())), |
||||||
|
MinEdgeCount: 3, |
||||||
|
MaxEdgeCount: 8, |
||||||
|
}) |
||||||
|
|
||||||
|
rand.Seed(time.Now().Unix()) |
||||||
|
|
||||||
|
if useSketch { |
||||||
|
for i := 0; i < totalCycleCount; i++ { |
||||||
|
s.Update() |
||||||
|
} |
||||||
|
|
||||||
|
saveOutput(s.Output(), outputImgName) |
||||||
|
} else { |
||||||
|
|
||||||
|
saveOutput(img, outputImgName) |
||||||
|
} |
||||||
|
|
||||||
|
if setWallpaper { |
||||||
|
err = wallpaper.SetFromFile(outputImgName) |
||||||
|
if err != nil { |
||||||
|
log.Panicln(err) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func loadRandomUnsplashImage(width, height int) (image.Image, error) { |
||||||
|
url := fmt.Sprintf("https://source.unsplash.com/random/%dx%d", width, height) |
||||||
|
res, err := http.Get(url) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
defer res.Body.Close() |
||||||
|
|
||||||
|
img, _, err := image.Decode(res.Body) |
||||||
|
return img, err |
||||||
|
} |
||||||
|
|
||||||
|
func loadImage(src string) (image.Image, error) { |
||||||
|
file, _ := os.Open(sourceImgName) |
||||||
|
defer file.Close() |
||||||
|
img, _, err := image.Decode(file) |
||||||
|
return img, err |
||||||
|
} |
||||||
|
|
||||||
|
func saveOutput(img image.Image, filePath string) error { |
||||||
|
f, err := os.Create(filePath) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
defer f.Close() |
||||||
|
|
||||||
|
// Encode to `PNG` with `DefaultCompression` level
|
||||||
|
// then save to file
|
||||||
|
err = png.Encode(f, img) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// https://stackoverflow.com/questions/33845770/how-do-i-download-a-file-with-a-http-request-in-go-language/33845771
|
||||||
|
func downloadFile(filepath string, url string) (err error) { |
||||||
|
|
||||||
|
// Create the file
|
||||||
|
out, err := os.Create(filepath) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
defer out.Close() |
||||||
|
|
||||||
|
// Get the data
|
||||||
|
resp, err := http.Get(url) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
defer resp.Body.Close() |
||||||
|
|
||||||
|
// Writer the body to file
|
||||||
|
_, err = io.Copy(out, resp.Body) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,92 @@ |
|||||||
|
package main |
||||||
|
|
||||||
|
// imports
|
||||||
|
import ( |
||||||
|
"github.com/fogleman/gg" |
||||||
|
"image" |
||||||
|
"image/color" |
||||||
|
"math/rand" |
||||||
|
) |
||||||
|
|
||||||
|
// imports
|
||||||
|
|
||||||
|
type UserParams struct { |
||||||
|
StrokeRatio float64 |
||||||
|
DestWidth int |
||||||
|
DestHeight int |
||||||
|
InitialAlpha float64 |
||||||
|
StrokeReduction float64 |
||||||
|
AlphaIncrease float64 |
||||||
|
StrokeInversionThreshold float64 |
||||||
|
StrokeJitter int |
||||||
|
MinEdgeCount int |
||||||
|
MaxEdgeCount int |
||||||
|
} |
||||||
|
|
||||||
|
type Sketch struct { |
||||||
|
UserParams // embed for easier access
|
||||||
|
source image.Image |
||||||
|
dc *gg.Context |
||||||
|
sourceWidth int |
||||||
|
sourceHeight int |
||||||
|
strokeSize float64 |
||||||
|
initialStrokeSize float64 |
||||||
|
} |
||||||
|
|
||||||
|
func NewSketch(source image.Image, userParams UserParams) *Sketch { |
||||||
|
s := &Sketch{UserParams: userParams} |
||||||
|
bounds := source.Bounds() |
||||||
|
s.sourceWidth, s.sourceHeight = bounds.Max.X, bounds.Max.Y |
||||||
|
s.initialStrokeSize = s.StrokeRatio * float64(s.DestWidth) |
||||||
|
s.strokeSize = s.initialStrokeSize |
||||||
|
|
||||||
|
canvas := gg.NewContext(s.DestWidth, s.DestHeight) |
||||||
|
canvas.SetColor(color.Black) |
||||||
|
canvas.DrawRectangle(0, 0, float64(s.DestWidth), float64(s.DestHeight)) |
||||||
|
canvas.FillPreserve() |
||||||
|
|
||||||
|
s.source = source |
||||||
|
s.dc = canvas |
||||||
|
return s |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Sketch) Update() { |
||||||
|
rndX := rand.Float64() * float64(s.sourceWidth) |
||||||
|
rndY := rand.Float64() * float64(s.sourceHeight) |
||||||
|
r, g, b := rgb255(s.source.At(int(rndX), int(rndY))) |
||||||
|
|
||||||
|
destX := rndX * float64(s.DestWidth) / float64(s.sourceWidth) |
||||||
|
destX += float64(randRange(s.StrokeJitter)) |
||||||
|
destY := rndY * float64(s.DestHeight) / float64(s.sourceHeight) |
||||||
|
destY += float64(randRange(s.StrokeJitter)) |
||||||
|
edges := s.MinEdgeCount + rand.Intn(s.MaxEdgeCount-s.MinEdgeCount+1) |
||||||
|
|
||||||
|
s.dc.SetRGBA255(r, g, b, int(s.InitialAlpha)) |
||||||
|
s.dc.DrawRegularPolygon(edges, destX, destY, s.strokeSize, rand.Float64()) |
||||||
|
s.dc.FillPreserve() |
||||||
|
|
||||||
|
if s.strokeSize <= s.StrokeInversionThreshold*s.initialStrokeSize { |
||||||
|
if (r+g+b)/3 < 128 { |
||||||
|
s.dc.SetRGBA255(255, 255, 255, int(s.InitialAlpha*2)) |
||||||
|
} else { |
||||||
|
s.dc.SetRGBA255(0, 0, 0, int(s.InitialAlpha*2)) |
||||||
|
} |
||||||
|
} |
||||||
|
s.dc.Stroke() |
||||||
|
|
||||||
|
s.strokeSize -= s.StrokeReduction * s.strokeSize |
||||||
|
s.InitialAlpha += s.AlphaIncrease |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Sketch) Output() image.Image { |
||||||
|
return s.dc.Image() |
||||||
|
} |
||||||
|
|
||||||
|
func rgb255(c color.Color) (r, g, b int) { |
||||||
|
r0, g0, b0, _ := c.RGBA() |
||||||
|
return int(r0 / 257), int(g0 / 257), int(b0 / 257) |
||||||
|
} |
||||||
|
|
||||||
|
func randRange(max int) int { |
||||||
|
return -max + rand.Intn(2*max) |
||||||
|
} |
@ -0,0 +1,33 @@ |
|||||||
|
package main |
||||||
|
|
||||||
|
type APODResponse struct { |
||||||
|
Date string `json:"date"` |
||||||
|
Explanation string `json:"explanation"` |
||||||
|
Hdurl string `json:"hdurl"` |
||||||
|
MediaType string `json:"media_type"` |
||||||
|
ServiceVersion string `json:"service_version"` |
||||||
|
Title string `json:"title"` |
||||||
|
URL string `json:"url"` |
||||||
|
} |
||||||
|
|
||||||
|
type MarsResponse struct { |
||||||
|
Photos []struct { |
||||||
|
ID int `json:"id"` |
||||||
|
Sol int `json:"sol"` |
||||||
|
Camera struct { |
||||||
|
ID int `json:"id"` |
||||||
|
Name string `json:"name"` |
||||||
|
RoverID int `json:"rover_id"` |
||||||
|
FullName string `json:"full_name"` |
||||||
|
} `json:"camera"` |
||||||
|
ImgSrc string `json:"img_src"` |
||||||
|
EarthDate string `json:"earth_date"` |
||||||
|
Rover struct { |
||||||
|
ID int `json:"id"` |
||||||
|
Name string `json:"name"` |
||||||
|
LandingDate string `json:"landing_date"` |
||||||
|
LaunchDate string `json:"launch_date"` |
||||||
|
Status string `json:"status"` |
||||||
|
} `json:"rover"` |
||||||
|
} `json:"photos"` |
||||||
|
} |
Loading…
Reference in new issue