/
mediaItem.go
163 lines (148 loc) · 4.9 KB
/
mediaItem.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
package googlephotos
import (
"crypto/md5"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"time"
"github.com/andrewjjenkins/picsync/pkg/cache"
)
type MediaItem struct {
Id string `json:"id"`
Description string `json:"description"`
ProductUrl string `json:"productUrl"`
BaseUrl string `json:"baseUrl"`
MimeType string `json:"mimeType"`
Filename string `json:"filename"`
MediaMetadata MediaMetadata `json:"mediaMetadata"`
// ContributorInfo ContributorInfo `json:"contributorInfo"`
}
type MediaMetadata struct {
CreationTime string `json:"creationTime"`
Width MaybeQuotedInt64 `json:"width"`
Height MaybeQuotedInt64 `json:"height"`
// Either Photo or Video will be present
Photo *PhotoMediaMetadata `json:"photo"`
Video *VideoMediaMetadata `json:"video"`
}
type PhotoMediaMetadata struct {
CameraMake string `json:"cameraMake"`
CameraModel string `json:"cameraModel"`
FocalLength float64 `json:"focalLength"`
ApertureFNumber float64 `json:"apertureFNumber"`
IsoEquivalent MaybeQuotedInt64 `json:"isoEquivalent"`
ExposureTime string `json:"exposureTime"`
}
type VideoMediaMetadata struct {
CameraMake string `json:"cameraMake"`
CameraModel string `json:"cameraModel"`
Fps float64 `json:"fps"`
Status string `json:"status"`
}
type CachedMediaItem struct {
CacheId int64
Sha256 string
Md5 string
LastUpdated time.Time
LastUsed time.Time
MediaItem *MediaItem
}
type UpdateCacheResult struct {
CachedMediaItems []*CachedMediaItem
NextPageToken string
}
type UpdateCacheCallback func(*CachedMediaItem)
func (c *clientImpl) UpdateCacheForAlbumId(albumId string, nextPageToken string, cb UpdateCacheCallback) (*UpdateCacheResult, error) {
res, err := c.ListMediaItemsForAlbumId(albumId, nextPageToken)
toRet := &UpdateCacheResult{}
if err != nil {
return nil, err
}
toRet.NextPageToken = res.NextPageToken
for _, item := range res.MediaItems {
// First, see if it is already in the cache. Google never changes
// the contents of a Google Photos ID, so if it is already present we don't
// need to download it again.
currentEntry, err := c.cache.GetGooglephoto(item.Id)
if err != nil {
return nil, err
}
if currentEntry != nil {
// Update the timestamps and set back to the cache.
currentEntry.LastUpdated = time.Now()
currentEntry.LastUsed = currentEntry.LastUpdated
err = c.cache.UpsertGooglephoto(currentEntry)
if err != nil {
return nil, err
}
cached := CachedMediaItem{
CacheId: currentEntry.Id,
Sha256: currentEntry.Sha256,
Md5: currentEntry.Md5,
LastUpdated: currentEntry.LastUpdated,
LastUsed: currentEntry.LastUsed,
MediaItem: item,
}
toRet.CachedMediaItems = append(toRet.CachedMediaItems, &cached)
cb(&cached)
continue
}
// Item not in the cache. We must download it and calculate hashes.
fullResUrl := item.BaseUrl + "=d"
resp, err := http.Get(fullResUrl)
if err != nil {
// FIXME: Maybe we want to skip updating cache for this item if we
// just have a download error rather than failing the entire call?
c.prom.mediaItemsDownloadedFailure.Inc()
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
c.prom.mediaItemsDownloadedFailure.Inc()
return nil, fmt.Errorf("received HTTP %d", resp.StatusCode)
}
sha256Hash := sha256.New()
md5Hash := md5.New()
allHashes := io.MultiWriter(sha256Hash, md5Hash)
if _, err := io.Copy(allHashes, resp.Body); err != nil {
c.prom.mediaItemsDownloadedFailure.Inc()
return nil, err
}
c.prom.mediaItemsDownloadedSuccess.Inc()
// FIXME: resp.ContentLength may in theory be unknown, but is known
// for google photos. A safer approach would be to make another
// member of the io.Multiwriter() that just counted bytes and threw them
// on the ground, and then ask it how many bytes we saw.
if resp.ContentLength > 0 {
c.prom.mediaItemsDownloadedBytes.Add(float64(resp.ContentLength))
}
entry := cache.GooglephotoData{
BaseUrl: item.BaseUrl,
GooglephotosId: item.Id,
Sha256: hex.EncodeToString(sha256Hash.Sum(nil)),
Md5: hex.EncodeToString(md5Hash.Sum(nil)),
Width: int64(item.MediaMetadata.Width),
Height: int64(item.MediaMetadata.Height),
LastUpdated: time.Now(),
LastUsed: time.Now(),
}
err = c.cache.UpsertGooglephoto(&entry)
if err != nil {
// FIXME: Again, maybe just skip individual errors?
return nil, err
}
cached := CachedMediaItem{
CacheId: entry.Id,
Sha256: entry.Sha256,
Md5: entry.Md5,
LastUpdated: entry.LastUpdated,
LastUsed: entry.LastUsed,
MediaItem: item,
}
toRet.CachedMediaItems = append(toRet.CachedMediaItems, &cached)
cb(&cached)
}
return toRet, nil
}