Adding persistent storage and working auth
Signed-off-by: Kris Nóva <kris@nivenly.com>
This commit is contained in:
parent
ba8188c7b9
commit
8829212093
9 changed files with 259 additions and 96 deletions
|
@ -1,5 +1,32 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
type V1Client struct {
|
type V1Client struct {
|
||||||
//
|
token string
|
||||||
|
connectionString string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v1 *V1Client) SetConnectionString(connection string) {
|
||||||
|
v1.connectionString = connection
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v1 *V1Client) SetToken(token string) {
|
||||||
|
v1.token = token
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v1 *V1Client) GET(format string, a ...interface{}) (*http.Response, error) {
|
||||||
|
url := v1.Endpoint(fmt.Sprintf(format, a))
|
||||||
|
return http.Get(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v1 *V1Client) Endpoint(str string) string {
|
||||||
|
if strings.HasPrefix("/", str) {
|
||||||
|
return fmt.Sprintf("%s%s", v1.connectionString, str)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s/%s", v1.connectionString, str)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -71,18 +73,37 @@ type Photo struct {
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
// uuid: string PhotoUID as returned by the API
|
// uuid: string PhotoUID as returned by the API
|
||||||
func (c *V1Client) GetPhoto(uuid string) (*Photo, error) {
|
func (v1 *V1Client) GetPhoto(uuid string) (*Photo, error) {
|
||||||
if uuid == "" {
|
if uuid == "" {
|
||||||
return nil, fmt.Errorf("missing uuid for GetPhoto [GET /api/v1/photos/:uuid]")
|
return nil, fmt.Errorf("missing uuid for GetPhoto [GET /api/v1/photos/:uuid]")
|
||||||
}
|
}
|
||||||
photo := &Photo{
|
resp, err := v1.GET("photos/%s", uuid)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to get photo uuid=%s with error: %v", uuid, err)
|
||||||
|
}
|
||||||
|
photo := Photo{
|
||||||
UUID: uuid,
|
UUID: uuid,
|
||||||
}
|
}
|
||||||
return photo, nil
|
bytes, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to parse body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The API returns HTML so we have to hack this shit up
|
||||||
|
// TODO @kris-nova This is where we left off
|
||||||
|
// TODO It looks like the API is returning HTML SMDH...
|
||||||
|
//fmt.Println(string(bytes))
|
||||||
|
//return nil, nil
|
||||||
|
|
||||||
|
err = json.Unmarshal(bytes, &photo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to JSON unmarshal response body: %v", err)
|
||||||
|
}
|
||||||
|
return &photo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PUT /api/v1/photos/:uid
|
// PUT /api/v1/photos/:uid
|
||||||
func (c *V1Client) UpdatePhoto(update *Photo) (*Photo, error) {
|
func (v1 *V1Client) UpdatePhoto(update *Photo) (*Photo, error) {
|
||||||
if update.UUID == "" {
|
if update.UUID == "" {
|
||||||
return nil, fmt.Errorf("missing uuid for UpdatePhoto [PUT /api/v1/photos/:uid]")
|
return nil, fmt.Errorf("missing uuid for UpdatePhoto [PUT /api/v1/photos/:uid]")
|
||||||
}
|
}
|
||||||
|
@ -96,7 +117,7 @@ func (c *V1Client) UpdatePhoto(update *Photo) (*Photo, error) {
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
// uuid: string PhotoUUID as returned by the API
|
// uuid: string PhotoUUID as returned by the API
|
||||||
func (c *V1Client) GetPhotoDownload(uuid string) (*File, error) {
|
func (v1 *V1Client) GetPhotoDownload(uuid string) (*File, error) {
|
||||||
if uuid == "" {
|
if uuid == "" {
|
||||||
return nil, fmt.Errorf("missing uuid for GetPhotoDownload [GET /api/v1/photos/:uuid/dl]")
|
return nil, fmt.Errorf("missing uuid for GetPhotoDownload [GET /api/v1/photos/:uuid/dl]")
|
||||||
}
|
}
|
||||||
|
@ -108,7 +129,7 @@ func (c *V1Client) GetPhotoDownload(uuid string) (*File, error) {
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
// uuid: string PhotoUUID as returned by the API
|
// uuid: string PhotoUUID as returned by the API
|
||||||
func (c *V1Client) GetPhotoYaml(uuid string) (*Photo, error) {
|
func (v1 *V1Client) GetPhotoYaml(uuid string) (*Photo, error) {
|
||||||
if uuid == "" {
|
if uuid == "" {
|
||||||
return nil, fmt.Errorf("missing uuid for GetPhotoYAML [GET /api/v1/photos/:uuid/yaml]")
|
return nil, fmt.Errorf("missing uuid for GetPhotoYAML [GET /api/v1/photos/:uuid/yaml]")
|
||||||
}
|
}
|
||||||
|
@ -120,7 +141,7 @@ func (c *V1Client) GetPhotoYaml(uuid string) (*Photo, error) {
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
// uuid: string PhotoUUID as returned by the API
|
// uuid: string PhotoUUID as returned by the API
|
||||||
func (c *V1Client) ApprovePhoto(uuid string) (*Photo, error) {
|
func (v1 *V1Client) ApprovePhoto(uuid string) (*Photo, error) {
|
||||||
if uuid == "" {
|
if uuid == "" {
|
||||||
return nil, fmt.Errorf("missing uuid for ApprovePhoto [POST /api/v1/photos/:uuid/approve]")
|
return nil, fmt.Errorf("missing uuid for ApprovePhoto [POST /api/v1/photos/:uuid/approve]")
|
||||||
}
|
}
|
||||||
|
@ -132,7 +153,7 @@ func (c *V1Client) ApprovePhoto(uuid string) (*Photo, error) {
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
// uid: string PhotoUID as returned by the API
|
// uid: string PhotoUID as returned by the API
|
||||||
func (c *V1Client) LikePhoto(uuid string) error {
|
func (v1 *V1Client) LikePhoto(uuid string) error {
|
||||||
if uuid == "" {
|
if uuid == "" {
|
||||||
return fmt.Errorf("missing uuid for LikePhoto [POST /api/v1/photos/:uid/like]")
|
return fmt.Errorf("missing uuid for LikePhoto [POST /api/v1/photos/:uid/like]")
|
||||||
}
|
}
|
||||||
|
@ -143,7 +164,7 @@ func (c *V1Client) LikePhoto(uuid string) error {
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
// uuid: string PhotoUUID as returned by the API
|
// uuid: string PhotoUUID as returned by the API
|
||||||
func (c *V1Client) DislikePhoto(uuid string) error {
|
func (v1 *V1Client) DislikePhoto(uuid string) error {
|
||||||
if uuid == "" {
|
if uuid == "" {
|
||||||
return fmt.Errorf("missing uuid for DislikePhoto [DELETE /api/v1/photos/:uuid/like]")
|
return fmt.Errorf("missing uuid for DislikePhoto [DELETE /api/v1/photos/:uuid/like]")
|
||||||
}
|
}
|
||||||
|
@ -155,7 +176,7 @@ func (c *V1Client) DislikePhoto(uuid string) error {
|
||||||
// Parameters:
|
// Parameters:
|
||||||
// uid: string PhotoUID as returned by the API
|
// uid: string PhotoUID as returned by the API
|
||||||
// file_uid: string File UID as returned by the API
|
// file_uid: string File UID as returned by the API
|
||||||
func (c *V1Client) PhotoPrimary(uuid, fileuuid string) error {
|
func (v1 *V1Client) PhotoPrimary(uuid, fileuuid string) error {
|
||||||
if uuid == "" {
|
if uuid == "" {
|
||||||
return fmt.Errorf("missing uuid for PhotoPrimary [POST /api/v1/photos/:uid/files/:file_uid/primary]")
|
return fmt.Errorf("missing uuid for PhotoPrimary [POST /api/v1/photos/:uid/files/:file_uid/primary]")
|
||||||
}
|
}
|
||||||
|
@ -164,3 +185,40 @@ func (c *V1Client) PhotoPrimary(uuid, fileuuid string) error {
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----
|
||||||
|
// Dump from Chrome
|
||||||
|
//
|
||||||
|
//Request URL: http://localhost:8080/api/v1/photos/pqnzigq156lndozm
|
||||||
|
//Request Method: PUT
|
||||||
|
//Status Code: 200 OK
|
||||||
|
//Remote Address: 127.0.0.1:8080
|
||||||
|
//Referrer Policy: strict-origin-when-cross-origin
|
||||||
|
|
||||||
|
// [RESPONSE HEADERS]
|
||||||
|
//Content-Type: application/json; charset=utf-8
|
||||||
|
//Date: Thu, 04 Feb 2021 04:27:16 GMT
|
||||||
|
//Transfer-Encoding: chunked
|
||||||
|
|
||||||
|
// [REQUEST HEADERS]
|
||||||
|
//Accept: application/json, text/plain, */*
|
||||||
|
//Accept-Encoding: gzip, deflate, br
|
||||||
|
//Accept-Language: en-US,en;q=0.9
|
||||||
|
//Connection: keep-alive
|
||||||
|
//Content-Length: 41
|
||||||
|
//Content-Type: application/json;charset=UTF-8
|
||||||
|
//Host: localhost:8080
|
||||||
|
//Origin: http://localhost:8080
|
||||||
|
//Referer: http://localhost:8080/albums/aqnzih81icziiyae/february-2021
|
||||||
|
//Sec-Fetch-Dest: empty
|
||||||
|
//Sec-Fetch-Mode: cors
|
||||||
|
//Sec-Fetch-Site: same-origin
|
||||||
|
//User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36
|
||||||
|
//X-Client-Hash: 2607a5a5
|
||||||
|
//X-Client-Version: 210121-07e559df-Linux-x86_64
|
||||||
|
//X-Session-ID: d92837cb1c41e37b9993d25e282efb3b337b6ae609a687d9
|
||||||
|
|
||||||
|
// [REQUEST PAYLOAD]
|
||||||
|
//{Title: "Test Nova", TitleSrc: "manual"}
|
||||||
|
//Title: "Test Nova"
|
||||||
|
//TitleSrc: "manual"
|
||||||
|
|
164
client.go
164
client.go
|
@ -1,65 +1,159 @@
|
||||||
package photoprism
|
package photoprism
|
||||||
|
|
||||||
import "github.com/kris-nova/client-go/api/v1"
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
type Client struct {
|
"github.com/kris-nova/client-go/api/v1"
|
||||||
v1client *api.V1Client
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DefaultContentType is the content type header the API expects
|
||||||
|
DefaultContentType string = "application/json; charset=utf-8"
|
||||||
|
|
||||||
|
// Default Host Configuration
|
||||||
|
DefaultHost string = "localhost"
|
||||||
|
DefaultLoopback string = "127.0.0.1"
|
||||||
|
DefaultPort string = "8080"
|
||||||
|
DefaultConnectionString string = "http://localhost:8080"
|
||||||
|
)
|
||||||
|
|
||||||
|
// New is used to initialize a new *Client
|
||||||
|
func New(auth ClientAuthenticator) *Client {
|
||||||
|
|
||||||
|
p := &Client{
|
||||||
|
v1client: &api.V1Client{},
|
||||||
|
authenticator: auth,
|
||||||
|
contentType: DefaultContentType,
|
||||||
|
connectionString: DefaultConnectionString,
|
||||||
|
}
|
||||||
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Client represents a client to a Photoprism application
|
||||||
|
type Client struct {
|
||||||
|
v1client *api.V1Client
|
||||||
|
authenticator ClientAuthenticator
|
||||||
|
contentType string
|
||||||
|
connectionString string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClientAuthenticator is used to store the secret
|
||||||
|
// data for authenticating with the Photoprism API
|
||||||
|
//
|
||||||
|
// TODO @kris-nova obfuscate the data, and make immutable and unexported fields
|
||||||
type ClientAuthenticator interface {
|
type ClientAuthenticator interface {
|
||||||
getKey() string
|
getKey() string
|
||||||
getSecret() string
|
getSecret() string
|
||||||
|
JSON() ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- [ ClientAuthLogin ] --
|
// -- [ ClientAuthLogin ] --
|
||||||
|
|
||||||
// TODO We probably want to base64 encode this
|
|
||||||
type ClientAuthLogin struct {
|
type ClientAuthLogin struct {
|
||||||
user string
|
// Request
|
||||||
pass string
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
|
||||||
|
// Response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewClientAuthLogin is used to build a new login struct
|
||||||
func NewClientAuthLogin(user, pass string) ClientAuthenticator {
|
func NewClientAuthLogin(user, pass string) ClientAuthenticator {
|
||||||
return &ClientAuthLogin{
|
return &ClientAuthLogin{
|
||||||
user: user,
|
Username: user,
|
||||||
pass: pass,
|
Password: pass,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ClientAuthLogin) getKey() string {
|
func (c *ClientAuthLogin) getKey() string {
|
||||||
return c.user
|
return c.Username
|
||||||
}
|
}
|
||||||
func (c *ClientAuthLogin) getSecret() string {
|
func (c *ClientAuthLogin) getSecret() string {
|
||||||
return c.pass
|
return c.Password
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- [ ClientAuthToken ] --
|
// JSON is used to marshal the fields to JSON
|
||||||
|
func (c *ClientAuthLogin) JSON() ([]byte, error) {
|
||||||
// TODO We probably want to base64 encode this
|
return json.Marshal(c)
|
||||||
type ClientAuthToken struct {
|
|
||||||
key string
|
|
||||||
secret string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewClientAuthToken(key, secret string) ClientAuthenticator {
|
|
||||||
return &ClientAuthToken{
|
|
||||||
key: key,
|
|
||||||
secret: secret,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientAuthToken) getKey() string {
|
|
||||||
return c.key
|
|
||||||
}
|
|
||||||
func (c *ClientAuthToken) getSecret() string {
|
|
||||||
return c.secret
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(auth ClientAuthenticator) *Client {
|
|
||||||
p := &Client{}
|
|
||||||
return p
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// V1 is used to access the V1 version of the Photoprism API
|
||||||
func (c *Client) V1() *api.V1Client {
|
func (c *Client) V1() *api.V1Client {
|
||||||
return c.v1client
|
return c.v1client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Login is used to attempt to authenticate with the Photoprism API
|
||||||
|
func (c *Client) Login() error {
|
||||||
|
// @kris-nova We are returning V1 by default
|
||||||
|
return c.LoginV1()
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/v1/session
|
||||||
|
//
|
||||||
|
// Data: {username: "admin", password: "missy"}
|
||||||
|
func (c *Client) LoginV1() error {
|
||||||
|
body, err := c.authenticator.JSON()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("JSON marshal error: %v", err)
|
||||||
|
}
|
||||||
|
buffer := bytes.NewBuffer(body)
|
||||||
|
resp, err := http.Post(c.Endpoint("v1/session"), c.contentType, buffer)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("authentication error: %v", err)
|
||||||
|
}
|
||||||
|
token := resp.Header.Get("X-Session-Id")
|
||||||
|
c.v1client.SetToken(token)
|
||||||
|
c.v1client.SetConnectionString(c.connectionString)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Endpoint is used to calculate a FQN for a given API endpoint
|
||||||
|
// based on the API version and Host/Port
|
||||||
|
func (c *Client) Endpoint(str string) string {
|
||||||
|
if strings.HasPrefix("/", str) {
|
||||||
|
return fmt.Sprintf("%s%s", c.connectionString, str)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s/%s", c.connectionString, str)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------------------
|
||||||
|
// Dump from Chrome/Network
|
||||||
|
|
||||||
|
// [REQUEST]
|
||||||
|
//Request URL: http://localhost:8080/api/v1/session
|
||||||
|
//Request Method: POST
|
||||||
|
//Status Code: 200 OK
|
||||||
|
//Remote Address: 127.0.0.1:8080
|
||||||
|
//Referrer Policy: strict-origin-when-cross-origin
|
||||||
|
|
||||||
|
// [RESPONSE HEADERS]
|
||||||
|
//Content-Type: application/json; charset=utf-8
|
||||||
|
//Date: Thu, 04 Feb 2021 03:27:03 GMT
|
||||||
|
//Transfer-Encoding: chunked
|
||||||
|
//X-Session-Id: d92837cb1c41e37b9993d25e282efb3b337b6ae609a687d9
|
||||||
|
|
||||||
|
// [REQUEST HEADERS]
|
||||||
|
//Accept: application/json, text/plain, */*
|
||||||
|
//Accept-Encoding: gzip, deflate, br
|
||||||
|
//Accept-Language: en-US,en;q=0.9
|
||||||
|
//Connection: keep-alive
|
||||||
|
//Content-Length: 39
|
||||||
|
//Content-Type: application/json;charset=UTF-8
|
||||||
|
//Host: localhost:8080
|
||||||
|
//Origin: http://localhost:8080
|
||||||
|
//Referer: http://localhost:8080/login
|
||||||
|
//Sec-Fetch-Dest: empty
|
||||||
|
//Sec-Fetch-Mode: cors
|
||||||
|
//Sec-Fetch-Site: same-origin
|
||||||
|
//User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36
|
||||||
|
//X-Client-Hash: 2607a5a5
|
||||||
|
//X-Client-Version: 210121-07e559df-Linux-x86_64
|
||||||
|
|
||||||
|
// [POST DATA]
|
||||||
|
//{username: "admin", password: "missy"}
|
||||||
|
//password: "missy"
|
||||||
|
//username: "admin"
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
sampleapp "github.com/kris-nova/client-go/sample-app"
|
|
||||||
"github.com/kris-nova/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
logger.Level = 4
|
|
||||||
app := sampleapp.New()
|
|
||||||
var err error
|
|
||||||
err = app.Create()
|
|
||||||
if err != nil {
|
|
||||||
logger.Critical(err.Error())
|
|
||||||
}
|
|
||||||
err = app.Start()
|
|
||||||
if err != nil {
|
|
||||||
logger.Critical(err.Error())
|
|
||||||
}
|
|
||||||
err = app.Stop()
|
|
||||||
if err != nil {
|
|
||||||
logger.Critical(err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
photoprism "github.com/kris-nova/client-go"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
uuid := os.Getenv("PHOTOPRISM_UUID")
|
|
||||||
if uuid == "" {
|
|
||||||
halt(2, "Missing PHOTOPRISM_UUID")
|
|
||||||
}
|
|
||||||
client := photoprism.New(auth())
|
|
||||||
photo, err := client.V1().GetPhoto(uuid)
|
|
||||||
if err != nil {
|
|
||||||
halt(3, "Error fetching photo: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println(*photo)
|
|
||||||
}
|
|
28
examples/photo.go
Normal file
28
examples/photo.go
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
photoprism "github.com/kris-nova/client-go"
|
||||||
|
"github.com/kris-nova/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
uuid := "pqnzigq156lndozm" // This is a known ID
|
||||||
|
client := photoprism.New(auth())
|
||||||
|
err := client.Login()
|
||||||
|
if err != nil {
|
||||||
|
halt(4, "Error logging into API: %v", err)
|
||||||
|
}
|
||||||
|
logger.Always("Login Success!")
|
||||||
|
photo, err := client.V1().GetPhoto(uuid)
|
||||||
|
if err != nil {
|
||||||
|
halt(3, "Error fetching photo: %v", err)
|
||||||
|
}
|
||||||
|
bytes, err := json.Marshal(photo)
|
||||||
|
if err != nil {
|
||||||
|
halt(5, "Error: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println(string(bytes))
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"e33287a631810f290267be9f8c3940b401bea9f58ffc2b22": {
|
"d92837cb1c41e37b9993d25e282efb3b337b6ae609a687d9": {
|
||||||
"user": "uqnzie01i1nypnt9",
|
"user": "uqnzie01i1nypnt9",
|
||||||
"tokens": null,
|
"tokens": null,
|
||||||
"expiration": 1613013394435050492
|
"expiration": 1613014023691734304
|
||||||
}
|
}
|
||||||
}
|
}
|
Binary file not shown.
|
@ -1,7 +1,8 @@
|
||||||
TakenAt: 2021-02-04T03:17:07Z
|
TakenAt: 2021-02-04T03:17:07Z
|
||||||
UID: pqnzigq156lndozm
|
UID: pqnzigq156lndozm
|
||||||
Type: image
|
Type: image
|
||||||
Title: Elgexeiu Aa Pqo
|
Title: Test Nova
|
||||||
|
TitleSrc: manual
|
||||||
OriginalName: ElgexEiU8AA-pQO
|
OriginalName: ElgexEiU8AA-pQO
|
||||||
Year: -1
|
Year: -1
|
||||||
Month: -1
|
Month: -1
|
||||||
|
@ -9,4 +10,5 @@ Day: -1
|
||||||
Details:
|
Details:
|
||||||
Keywords: blue, elgexeiu, portrait
|
Keywords: blue, elgexeiu, portrait
|
||||||
CreatedAt: 2021-02-04T03:17:14.668332772Z
|
CreatedAt: 2021-02-04T03:17:14.668332772Z
|
||||||
UpdatedAt: 2021-02-04T03:17:19.250144986Z
|
UpdatedAt: 2021-02-04T04:27:16.228466834Z
|
||||||
|
EditedAt: 2021-02-04T04:27:16Z
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue