summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--main.go412
-rw-r--r--public/gallery.js230
2 files changed, 321 insertions, 321 deletions
diff --git a/main.go b/main.go
index 29b08d3..53c7e22 100644
--- a/main.go
+++ b/main.go
@@ -453,212 +453,6 @@ func handleAPITags(w http.ResponseWriter, r *http.Request) {
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-func getImageDimensions(sha1 string) (w int64, h int64, err error) {
- err = db.QueryRow(`SELECT width, height FROM image WHERE sha1 = ?`,
- sha1).Scan(&w, &h)
- return
-}
-
-func getImagePaths(sha1 string) (paths []string, err error) {
- rows, err := db.Query(`WITH RECURSIVE paths(parent, path) AS (
- SELECT parent, name AS path FROM node WHERE sha1 = ?
- UNION ALL
- SELECT n.parent, n.name || '/' || p.path
- FROM node AS n JOIN paths AS p ON n.id = p.parent
- ) SELECT path FROM paths WHERE parent IS NULL`, sha1)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
-
- paths = []string{}
- for rows.Next() {
- var path string
- if err := rows.Scan(&path); err != nil {
- return nil, err
- }
- paths = append(paths, path)
- }
- return paths, rows.Err()
-}
-
-func getImageTags(sha1 string) (map[string]map[string]float32, error) {
- rows, err := db.Query(`
- SELECT ts.name, t.name, ta.weight FROM tag_assignment AS ta
- JOIN tag AS t ON t.id = ta.tag
- JOIN tag_space AS ts ON ts.id = t.space
- WHERE ta.sha1 = ?`, sha1)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
-
- result := make(map[string]map[string]float32)
- for rows.Next() {
- var (
- space, tag string
- weight float32
- )
- if err := rows.Scan(&space, &tag, &weight); err != nil {
- return nil, err
- }
-
- tags := result[space]
- if tags == nil {
- tags = make(map[string]float32)
- result[space] = tags
- }
- tags[tag] = weight
- }
- return result, rows.Err()
-}
-
-func handleAPIInfo(w http.ResponseWriter, r *http.Request) {
- var params struct {
- SHA1 string
- }
- if err := json.NewDecoder(r.Body).Decode(&params); err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- }
-
- var result struct {
- Width int64 `json:"width"`
- Height int64 `json:"height"`
- Paths []string `json:"paths"`
- Tags map[string]map[string]float32 `json:"tags"`
- }
-
- var err error
- result.Width, result.Height, err = getImageDimensions(params.SHA1)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- result.Paths, err = getImagePaths(params.SHA1)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- result.Tags, err = getImageTags(params.SHA1)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- if err := json.NewEncoder(w).Encode(result); err != nil {
- log.Println(err)
- }
-}
-
-// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-type webSimilarImage struct {
- SHA1 string `json:"sha1"`
- PixelsRatio float32 `json:"pixelsRatio"`
- ThumbW int64 `json:"thumbW"`
- ThumbH int64 `json:"thumbH"`
- Paths []string `json:"paths"`
-}
-
-func getSimilar(sha1 string, dhash int64, pixels int64, distance int) (
- result []webSimilarImage, err error) {
- // For distance ∈ {0, 1}, this query is quite inefficient.
- // In exchange, it's generic.
- //
- // If there's a dhash, there should also be thumbnail dimensions,
- // so not bothering with IFNULL on them.
- rows, err := db.Query(`
- SELECT sha1, width * height, IFNULL(thumbw, 0), IFNULL(thumbh, 0)
- FROM image WHERE sha1 <> ? AND dhash IS NOT NULL
- AND hamming(dhash, ?) = ?`, sha1, dhash, distance)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
-
- result = []webSimilarImage{}
- for rows.Next() {
- var (
- match webSimilarImage
- matchPixels int64
- )
- if err = rows.Scan(&match.SHA1,
- &matchPixels, &match.ThumbW, &match.ThumbH); err != nil {
- return nil, err
- }
- if match.Paths, err = getImagePaths(match.SHA1); err != nil {
- return nil, err
- }
- match.PixelsRatio = float32(matchPixels) / float32(pixels)
- result = append(result, match)
- }
- return result, rows.Err()
-}
-
-func getSimilarGroups(sha1 string, dhash int64, pixels int64,
- output map[string][]webSimilarImage) error {
- var err error
- for distance := 0; distance <= 1; distance++ {
- output[fmt.Sprintf("Perceptual distance %d", distance)], err =
- getSimilar(sha1, dhash, pixels, distance)
- if err != nil {
- return err
- }
- }
- return nil
-}
-
-func handleAPISimilar(w http.ResponseWriter, r *http.Request) {
- var params struct {
- SHA1 string
- }
- if err := json.NewDecoder(r.Body).Decode(&params); err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- }
-
- var result struct {
- Info webSimilarImage `json:"info"`
- Groups map[string][]webSimilarImage `json:"groups"`
- }
-
- result.Info = webSimilarImage{SHA1: params.SHA1, PixelsRatio: 1}
- if paths, err := getImagePaths(params.SHA1); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- } else {
- result.Info.Paths = paths
- }
-
- var (
- width, height int64
- dhash sql.NullInt64
- )
- err := db.QueryRow(`
- SELECT width, height, dhash, IFNULL(thumbw, 0), IFNULL(thumbh, 0)
- FROM image WHERE sha1 = ?`, params.SHA1).Scan(&width, &height, &dhash,
- &result.Info.ThumbW, &result.Info.ThumbH)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
-
- result.Groups = make(map[string][]webSimilarImage)
- if dhash.Valid {
- if err := getSimilarGroups(
- params.SHA1, dhash.Int64, width*height, result.Groups); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- }
-
- if err := json.NewEncoder(w).Encode(result); err != nil {
- log.Println(err)
- }
-}
-
-// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
type webDuplicateImage struct {
SHA1 string `json:"sha1"`
ThumbW int64 `json:"thumbW"`
@@ -899,6 +693,212 @@ func handleAPIOrphans(w http.ResponseWriter, r *http.Request) {
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+func getImageDimensions(sha1 string) (w int64, h int64, err error) {
+ err = db.QueryRow(`SELECT width, height FROM image WHERE sha1 = ?`,
+ sha1).Scan(&w, &h)
+ return
+}
+
+func getImagePaths(sha1 string) (paths []string, err error) {
+ rows, err := db.Query(`WITH RECURSIVE paths(parent, path) AS (
+ SELECT parent, name AS path FROM node WHERE sha1 = ?
+ UNION ALL
+ SELECT n.parent, n.name || '/' || p.path
+ FROM node AS n JOIN paths AS p ON n.id = p.parent
+ ) SELECT path FROM paths WHERE parent IS NULL`, sha1)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ paths = []string{}
+ for rows.Next() {
+ var path string
+ if err := rows.Scan(&path); err != nil {
+ return nil, err
+ }
+ paths = append(paths, path)
+ }
+ return paths, rows.Err()
+}
+
+func getImageTags(sha1 string) (map[string]map[string]float32, error) {
+ rows, err := db.Query(`
+ SELECT ts.name, t.name, ta.weight FROM tag_assignment AS ta
+ JOIN tag AS t ON t.id = ta.tag
+ JOIN tag_space AS ts ON ts.id = t.space
+ WHERE ta.sha1 = ?`, sha1)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ result := make(map[string]map[string]float32)
+ for rows.Next() {
+ var (
+ space, tag string
+ weight float32
+ )
+ if err := rows.Scan(&space, &tag, &weight); err != nil {
+ return nil, err
+ }
+
+ tags := result[space]
+ if tags == nil {
+ tags = make(map[string]float32)
+ result[space] = tags
+ }
+ tags[tag] = weight
+ }
+ return result, rows.Err()
+}
+
+func handleAPIInfo(w http.ResponseWriter, r *http.Request) {
+ var params struct {
+ SHA1 string
+ }
+ if err := json.NewDecoder(r.Body).Decode(&params); err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ var result struct {
+ Width int64 `json:"width"`
+ Height int64 `json:"height"`
+ Paths []string `json:"paths"`
+ Tags map[string]map[string]float32 `json:"tags"`
+ }
+
+ var err error
+ result.Width, result.Height, err = getImageDimensions(params.SHA1)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ result.Paths, err = getImagePaths(params.SHA1)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ result.Tags, err = getImageTags(params.SHA1)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ if err := json.NewEncoder(w).Encode(result); err != nil {
+ log.Println(err)
+ }
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+type webSimilarImage struct {
+ SHA1 string `json:"sha1"`
+ PixelsRatio float32 `json:"pixelsRatio"`
+ ThumbW int64 `json:"thumbW"`
+ ThumbH int64 `json:"thumbH"`
+ Paths []string `json:"paths"`
+}
+
+func getSimilar(sha1 string, dhash int64, pixels int64, distance int) (
+ result []webSimilarImage, err error) {
+ // For distance ∈ {0, 1}, this query is quite inefficient.
+ // In exchange, it's generic.
+ //
+ // If there's a dhash, there should also be thumbnail dimensions,
+ // so not bothering with IFNULL on them.
+ rows, err := db.Query(`
+ SELECT sha1, width * height, IFNULL(thumbw, 0), IFNULL(thumbh, 0)
+ FROM image WHERE sha1 <> ? AND dhash IS NOT NULL
+ AND hamming(dhash, ?) = ?`, sha1, dhash, distance)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ result = []webSimilarImage{}
+ for rows.Next() {
+ var (
+ match webSimilarImage
+ matchPixels int64
+ )
+ if err = rows.Scan(&match.SHA1,
+ &matchPixels, &match.ThumbW, &match.ThumbH); err != nil {
+ return nil, err
+ }
+ if match.Paths, err = getImagePaths(match.SHA1); err != nil {
+ return nil, err
+ }
+ match.PixelsRatio = float32(matchPixels) / float32(pixels)
+ result = append(result, match)
+ }
+ return result, rows.Err()
+}
+
+func getSimilarGroups(sha1 string, dhash int64, pixels int64,
+ output map[string][]webSimilarImage) error {
+ var err error
+ for distance := 0; distance <= 1; distance++ {
+ output[fmt.Sprintf("Perceptual distance %d", distance)], err =
+ getSimilar(sha1, dhash, pixels, distance)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func handleAPISimilar(w http.ResponseWriter, r *http.Request) {
+ var params struct {
+ SHA1 string
+ }
+ if err := json.NewDecoder(r.Body).Decode(&params); err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ var result struct {
+ Info webSimilarImage `json:"info"`
+ Groups map[string][]webSimilarImage `json:"groups"`
+ }
+
+ result.Info = webSimilarImage{SHA1: params.SHA1, PixelsRatio: 1}
+ if paths, err := getImagePaths(params.SHA1); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ } else {
+ result.Info.Paths = paths
+ }
+
+ var (
+ width, height int64
+ dhash sql.NullInt64
+ )
+ err := db.QueryRow(`
+ SELECT width, height, dhash, IFNULL(thumbw, 0), IFNULL(thumbh, 0)
+ FROM image WHERE sha1 = ?`, params.SHA1).Scan(&width, &height, &dhash,
+ &result.Info.ThumbW, &result.Info.ThumbH)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ result.Groups = make(map[string][]webSimilarImage)
+ if dhash.Valid {
+ if err := getSimilarGroups(
+ params.SHA1, dhash.Int64, width*height, result.Groups); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ }
+
+ if err := json.NewEncoder(w).Encode(result); err != nil {
+ log.Println(err)
+ }
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
// cmdWeb runs a web UI against GD on ADDRESS.
func cmdWeb(args []string) error {
if len(args) != 2 {
diff --git a/public/gallery.js b/public/gallery.js
index 4933064..52a92ab 100644
--- a/public/gallery.js
+++ b/public/gallery.js
@@ -253,6 +253,120 @@ let Tags = {
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+let DuplicatesModel = {
+ entries: [],
+
+ async reload() {
+ this.entries = await call('duplicates', {})
+ },
+}
+
+let DuplicatesThumbnail = {
+ view(vnode) {
+ const info = vnode.attrs.info
+ return [
+ m(m.route.Link, {href: `/similar/${info.sha1}`},
+ m(Thumbnail, {info})),
+ (info.occurences != 1) ? ` ×${info.occurences}` : [],
+ ]
+ },
+}
+
+let DuplicatesList = {
+ // See BrowseView.
+ oncreate(vnode) { vnode.dom.focus() },
+
+ view(vnode) {
+ let children = (DuplicatesModel.entries.length == 0)
+ ? "No duplicates"
+ : DuplicatesModel.entries.map(group =>
+ m('.row', group.map(entry =>
+ m(DuplicatesThumbnail, {info: entry}))))
+ return m('.duplicates[tabindex=0]', {}, children)
+ },
+}
+
+let Duplicates = {
+ oninit(vnode) {
+ DuplicatesModel.reload()
+ },
+
+ view(vnode) {
+ return m('.container', {}, [
+ m(Header),
+ m('.body', {}, m(DuplicatesList)),
+ ])
+ },
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+let OrphansModel = {
+ entries: [],
+
+ async reload() {
+ this.entries = await call('orphans', {})
+ },
+}
+
+let OrphansReplacement = {
+ view(vnode) {
+ const info = vnode.attrs.info
+ if (!info)
+ return []
+
+ return [
+ ` → `,
+ m(m.route.Link, {href: `/view/${info.sha1}`},
+ m(Thumbnail, {info})),
+ `${info.tags} tags`,
+ ]
+ },
+}
+
+let OrphansRow = {
+ view(vnode) {
+ const info = vnode.attrs.info
+ return m('.row', [
+ // It might not load, but still allow tag viewing.
+ m(m.route.Link, {href: `/view/${info.sha1}`},
+ m(Thumbnail, {info})),
+ `${info.tags} tags`,
+ m(OrphansReplacement, {info: info.replacement}),
+ ])
+ },
+}
+
+let OrphansList = {
+ // See BrowseView.
+ oncreate(vnode) { vnode.dom.focus() },
+
+ view(vnode) {
+ let children = (OrphansModel.entries.length == 0)
+ ? "No orphans"
+ : OrphansModel.entries.map(info => [
+ m("h2", info.lastPath),
+ m(OrphansRow, {info}),
+ ])
+ return m('.orphans[tabindex=0]', {}, children)
+ },
+}
+
+let Orphans = {
+ oninit(vnode) {
+ OrphansModel.reload()
+ },
+
+ view(vnode) {
+ return m('.container', {}, [
+ m(Header),
+ m('.body', {}, m(OrphansList)),
+ ])
+ },
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
let ViewModel = {
sha1: undefined,
width: 0,
@@ -427,120 +541,6 @@ let Similar = {
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-let DuplicatesModel = {
- entries: [],
-
- async reload() {
- this.entries = await call('duplicates', {})
- },
-}
-
-let DuplicatesThumbnail = {
- view(vnode) {
- const info = vnode.attrs.info
- return [
- m(m.route.Link, {href: `/similar/${info.sha1}`},
- m(Thumbnail, {info})),
- (info.occurences != 1) ? ` ×${info.occurences}` : [],
- ]
- },
-}
-
-let DuplicatesList = {
- // See BrowseView.
- oncreate(vnode) { vnode.dom.focus() },
-
- view(vnode) {
- let children = (DuplicatesModel.entries.length == 0)
- ? "No duplicates"
- : DuplicatesModel.entries.map(group =>
- m('.row', group.map(entry =>
- m(DuplicatesThumbnail, {info: entry}))))
- return m('.duplicates[tabindex=0]', {}, children)
- },
-}
-
-let Duplicates = {
- oninit(vnode) {
- DuplicatesModel.reload()
- },
-
- view(vnode) {
- return m('.container', {}, [
- m(Header),
- m('.body', {}, m(DuplicatesList)),
- ])
- },
-}
-
-// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-let OrphansModel = {
- entries: [],
-
- async reload() {
- this.entries = await call('orphans', {})
- },
-}
-
-let OrphansReplacement = {
- view(vnode) {
- const info = vnode.attrs.info
- if (!info)
- return []
-
- return [
- ` → `,
- m(m.route.Link, {href: `/view/${info.sha1}`},
- m(Thumbnail, {info})),
- `${info.tags} tags`,
- ]
- },
-}
-
-let OrphansRow = {
- view(vnode) {
- const info = vnode.attrs.info
- return m('.row', [
- // It might not load, but still allow tag viewing.
- m(m.route.Link, {href: `/view/${info.sha1}`},
- m(Thumbnail, {info})),
- `${info.tags} tags`,
- m(OrphansReplacement, {info: info.replacement}),
- ])
- },
-}
-
-let OrphansList = {
- // See BrowseView.
- oncreate(vnode) { vnode.dom.focus() },
-
- view(vnode) {
- let children = (OrphansModel.entries.length == 0)
- ? "No orphans"
- : OrphansModel.entries.map(info => [
- m("h2", info.lastPath),
- m(OrphansRow, {info}),
- ])
- return m('.orphans[tabindex=0]', {}, children)
- },
-}
-
-let Orphans = {
- oninit(vnode) {
- OrphansModel.reload()
- },
-
- view(vnode) {
- return m('.container', {}, [
- m(Header),
- m('.body', {}, m(OrphansList)),
- ])
- },
-}
-
-// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
window.addEventListener('load', () => {
m.route(document.body, "/browse/", {
// The path doesn't need to be escaped, perhaps change that (":key...").
@@ -554,6 +554,6 @@ window.addEventListener('load', () => {
"/view/:key": View,
"/similar/:key": Similar,
- "/search/:space/:tag": undefined,
+ "/search/:key": undefined,
})
})