Videos: Enforce transcoding to MPEG-4 AVC #603
This commit is contained in:
parent
f3d60ae095
commit
473566f404
|
@ -52,7 +52,7 @@
|
||||||
flat icon large absolute class="p-photo-select">
|
flat icon large absolute class="p-photo-select">
|
||||||
<v-icon color="white" class="t-select t-on">check_circle</v-icon>
|
<v-icon color="white" class="t-select t-on">check_circle</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn v-else-if="!selection.length && props.item.Type === 'video' && props.item.isPlayable()"
|
<v-btn v-else-if="!selection.length && (props.item.Type === 'video' || props.item.Type === 'live')"
|
||||||
:ripple="false"
|
:ripple="false"
|
||||||
flat icon large absolute class="p-photo-play opacity-75"
|
flat icon large absolute class="p-photo-play opacity-75"
|
||||||
@click.stop.prevent="openPhoto(props.index, true)">
|
@click.stop.prevent="openPhoto(props.index, true)">
|
||||||
|
|
|
@ -39,9 +39,13 @@ import {$gettext} from "common/vm";
|
||||||
|
|
||||||
export const SrcManual = "manual";
|
export const SrcManual = "manual";
|
||||||
export const CodecAvc1 = "avc1";
|
export const CodecAvc1 = "avc1";
|
||||||
export const TypeMP4 = "mp4";
|
export const FormatMp4 = "mp4";
|
||||||
export const TypeJpeg = "jpg";
|
export const FormatAvc = "avc";
|
||||||
|
export const FormatJpeg = "jpg";
|
||||||
export const TypeImage = "image";
|
export const TypeImage = "image";
|
||||||
|
export const TypeVideo= "video";
|
||||||
|
export const TypeLive = "live";
|
||||||
|
export const TypeRaw = "raw";
|
||||||
export const YearUnknown = -1;
|
export const YearUnknown = -1;
|
||||||
export const MonthUnknown = -1;
|
export const MonthUnknown = -1;
|
||||||
export const DayUnknown = -1;
|
export const DayUnknown = -1;
|
||||||
|
@ -260,7 +264,7 @@ export class Photo extends RestModel {
|
||||||
let file = this.Files.find(f => f.Codec === CodecAvc1);
|
let file = this.Files.find(f => f.Codec === CodecAvc1);
|
||||||
|
|
||||||
if (!file) {
|
if (!file) {
|
||||||
file = this.Files.find(f => f.Type === TypeMP4);
|
file = this.Files.find(f => f.Type === FormatMp4);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!file) {
|
if (!file) {
|
||||||
|
@ -274,10 +278,10 @@ export class Photo extends RestModel {
|
||||||
const file = this.videoFile();
|
const file = this.videoFile();
|
||||||
|
|
||||||
if (file) {
|
if (file) {
|
||||||
return `/api/v1/videos/${file.Hash}/${config.previewToken()}/${TypeMP4}`;
|
return `/api/v1/videos/${file.Hash}/${config.previewToken()}/${FormatAvc}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `/api/v1/videos/${this.Hash}/${config.previewToken()}/${TypeMP4}`;
|
return `/api/v1/videos/${this.Hash}/${config.previewToken()}/${FormatAvc}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
mainFile() {
|
mainFile() {
|
||||||
|
@ -288,7 +292,7 @@ export class Photo extends RestModel {
|
||||||
let file = this.Files.find(f => !!f.Primary);
|
let file = this.Files.find(f => !!f.Primary);
|
||||||
|
|
||||||
if (!file) {
|
if (!file) {
|
||||||
file = this.Files.find(f => f.Type === TypeJpeg);
|
file = this.Files.find(f => f.Type === FormatJpeg);
|
||||||
}
|
}
|
||||||
|
|
||||||
return file;
|
return file;
|
||||||
|
|
|
@ -2,6 +2,7 @@ import {$gettext} from "common/vm";
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import {Info} from "luxon";
|
import {Info} from "luxon";
|
||||||
import {config} from "../session";
|
import {config} from "../session";
|
||||||
|
import {TypeVideo,TypeImage,TypeLive,TypeRaw} from "../model/photo";
|
||||||
|
|
||||||
export const TimeZones = () => moment.tz.names();
|
export const TimeZones = () => moment.tz.names();
|
||||||
|
|
||||||
|
@ -189,19 +190,19 @@ export const MapsStyle = () => [
|
||||||
export const PhotoTypes = () => [
|
export const PhotoTypes = () => [
|
||||||
{
|
{
|
||||||
"text": $gettext("Image"),
|
"text": $gettext("Image"),
|
||||||
"value": "image",
|
"value": TypeImage,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"text": $gettext("Raw"),
|
"text": $gettext("Raw"),
|
||||||
"value": "raw",
|
"value": TypeRaw,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"text": $gettext("Live"),
|
"text": $gettext("Live"),
|
||||||
"value": "live",
|
"value": TypeLive,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"text": $gettext("Video"),
|
"text": $gettext("Video"),
|
||||||
"value": "video",
|
"value": TypeVideo,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Photo from "model/photo";
|
import {Photo,TypeVideo,TypeLive} from "model/photo";
|
||||||
import Album from "model/album";
|
import Album from "model/album";
|
||||||
import Event from "pubsub-js";
|
import Event from "pubsub-js";
|
||||||
import Thumb from "model/thumb";
|
import Thumb from "model/thumb";
|
||||||
|
@ -158,7 +158,7 @@ export default {
|
||||||
|
|
||||||
const selected = this.results[index];
|
const selected = this.results[index];
|
||||||
|
|
||||||
if (showMerged && (selected.Type === 'video' || selected.Type === 'live')) {
|
if (showMerged && (selected.Type === TypeVideo || selected.Type === TypeLive)) {
|
||||||
if (this.results[index].isPlayable()) {
|
if (this.results[index].isPlayable()) {
|
||||||
this.$modal.show('video', {video: selected, album: this.album});
|
this.$modal.show('video', {video: selected, album: this.album});
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Photo from "model/photo";
|
import {Photo, TypeVideo, TypeLive} from "model/photo";
|
||||||
import mapboxgl from "mapbox-gl";
|
import mapboxgl from "mapbox-gl";
|
||||||
import Api from "common/api";
|
import Api from "common/api";
|
||||||
import Thumb from "model/thumb";
|
import Thumb from "model/thumb";
|
||||||
|
@ -193,7 +193,7 @@ export default {
|
||||||
const index = this.photos.findIndex((p) => p.UID === id);
|
const index = this.photos.findIndex((p) => p.UID === id);
|
||||||
const selected = this.photos[index];
|
const selected = this.photos[index];
|
||||||
|
|
||||||
if (selected.Type === 'video' || selected.Type === 'live') {
|
if (selected.Type === TypeVideo || selected.Type === TypeLive) {
|
||||||
this.$modal.show('video', {video: selected, album: null});
|
this.$modal.show('video', {video: selected, album: null});
|
||||||
} else {
|
} else {
|
||||||
this.$viewer.show(Thumb.fromPhotos(this.photos), index)
|
this.$viewer.show(Thumb.fromPhotos(this.photos), index)
|
||||||
|
|
|
@ -48,7 +48,7 @@
|
||||||
flat icon large absolute class="p-photo-select">
|
flat icon large absolute class="p-photo-select">
|
||||||
<v-icon color="white" class="t-select t-on">check_circle</v-icon>
|
<v-icon color="white" class="t-select t-on">check_circle</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn v-else-if="!selection.length && props.item.Type === 'video' && props.item.isPlayable()"
|
<v-btn v-else-if="!selection.length && (props.item.Type === 'video' || props.item.Type === 'live')"
|
||||||
:ripple="false"
|
:ripple="false"
|
||||||
flat icon large absolute class="p-photo-play opacity-75"
|
flat icon large absolute class="p-photo-play opacity-75"
|
||||||
@click.stop.prevent="openPhoto(props.index, true)">
|
@click.stop.prevent="openPhoto(props.index, true)">
|
||||||
|
@ -174,7 +174,7 @@ export default {
|
||||||
} else if (this.photos[index]) {
|
} else if (this.photos[index]) {
|
||||||
let photo = this.photos[index];
|
let photo = this.photos[index];
|
||||||
|
|
||||||
if (photo.Type === 'video' && photo.isPlayable()) {
|
if (photo.Type === 'video' || photo.Type === 'live') {
|
||||||
this.openPhoto(index, true);
|
this.openPhoto(index, true);
|
||||||
} else {
|
} else {
|
||||||
this.openPhoto(index, false);
|
this.openPhoto(index, false);
|
||||||
|
|
|
@ -86,7 +86,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Photo from "model/photo";
|
import {Photo,TypeLive,TypeVideo} from "model/photo";
|
||||||
import Album from "model/album";
|
import Album from "model/album";
|
||||||
import Event from "pubsub-js";
|
import Event from "pubsub-js";
|
||||||
import Thumb from "model/thumb";
|
import Thumb from "model/thumb";
|
||||||
|
@ -204,7 +204,7 @@ export default {
|
||||||
|
|
||||||
const selected = this.results[index];
|
const selected = this.results[index];
|
||||||
|
|
||||||
if (showMerged && (selected.Type === 'video' || selected.Type === 'live')) {
|
if (showMerged && (selected.Type === TypeVideo || selected.Type === TypeLive)) {
|
||||||
if (this.results[index].isPlayable()) {
|
if (this.results[index].isPlayable()) {
|
||||||
this.$modal.show('video', {video: selected, album: this.album});
|
this.$modal.show('video', {video: selected, album: this.album});
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -390,18 +390,18 @@ describe("model/photo", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return video url", () => {
|
it("should return video url", () => {
|
||||||
const values = {ID: 8, UID: "ABC123", Filename: "1980/01/superCuteKitten.jpg", FileUID: "123fgb", Files: [{UID: "123fgb", Name: "1980/01/superCuteKitten.jpg", Primary: true, Type: "jpg", Width: 500, Height: 600, Hash: "1xxbgdt53"}]};
|
const values = {ID: 8, UID: "ABC123", Filename: "1980/01/superCuteKitten.jpg", Hash: "703cf8f274fbb265d49c6262825780e1", FileUID: "123fgb", Files: [{UID: "123fgb", Name: "1980/01/superCuteKitten.jpg", Primary: true, Type: "jpg", Width: 500, Height: 600, Hash: "1xxbgdt53"}]};
|
||||||
const photo = new Photo(values);
|
const photo = new Photo(values);
|
||||||
assert.equal(photo.videoUrl(), "");
|
assert.equal(photo.videoUrl(), "/api/v1/videos/703cf8f274fbb265d49c6262825780e1/static/avc");
|
||||||
const values2 = {ID: 9, UID: "ABC163"};
|
const values2 = {ID: 9, UID: "ABC163", Hash: "2305e512e3b183ec982d60a8b608a8ca501973ba"};
|
||||||
const photo2 = new Photo(values2);
|
const photo2 = new Photo(values2);
|
||||||
assert.equal(photo2.videoUrl(), false);
|
assert.equal(photo2.videoUrl(), "/api/v1/videos/2305e512e3b183ec982d60a8b608a8ca501973ba/static/avc");
|
||||||
const values3 = {ID: 10, UID: "ABC127", Filename: "1980/01/superCuteKitten.mp4", FileUID: "123fgb", Files: [{UID: "123fgb", Name: "1980/01/superCuteKitten.mp4", Primary: false, Type: "mp4", Width: 500, Height: 600, Hash: "1xxbgdt55"}]};
|
const values3 = {ID: 10, UID: "ABC127", Filename: "1980/01/superCuteKitten.mp4", FileUID: "123fgb", Files: [{UID: "123fgb", Name: "1980/01/superCuteKitten.mp4", Primary: false, Type: "mp4", Width: 500, Height: 600, Hash: "1xxbgdt55"}]};
|
||||||
const photo3 = new Photo(values3);
|
const photo3 = new Photo(values3);
|
||||||
assert.equal(photo3.videoUrl(), "/api/v1/videos/1xxbgdt55/static/mp4");
|
assert.equal(photo3.videoUrl(), "/api/v1/videos/1xxbgdt55/static/avc");
|
||||||
const values4 = {ID: 1, UID: "ABC128", Filename: "1980/01/superCuteKitten.jpg", FileUID: "123fgb", Files: [{UID: "123fgb", Name: "1980/01/superCuteKitten.jpg", Primary: false, Type: "jpg", Width: 500, Height: 600, Hash: "1xxbgdt53", Codec: "avc1"}]};
|
const values4 = {ID: 1, UID: "ABC128", Filename: "1980/01/superCuteKitten.jpg", FileUID: "123fgb", Files: [{UID: "123fgb", Name: "1980/01/superCuteKitten.jpg", Primary: false, Type: "jpg", Width: 500, Height: 600, Hash: "1xxbgdt53", Codec: "avc1"}]};
|
||||||
const photo4 = new Photo(values4);
|
const photo4 = new Photo(values4);
|
||||||
assert.equal(photo4.videoUrl(), "/api/v1/videos/1xxbgdt53/static/mp4");
|
assert.equal(photo4.videoUrl(), "/api/v1/videos/1xxbgdt53/static/avc");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return main file", () => {
|
it("should return main file", () => {
|
||||||
|
|
|
@ -2,6 +2,7 @@ package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/service"
|
"github.com/photoprism/photoprism/internal/service"
|
||||||
|
|
||||||
|
@ -27,7 +28,7 @@ func GetVideo(router *gin.RouterGroup) {
|
||||||
fileHash := c.Param("hash")
|
fileHash := c.Param("hash")
|
||||||
typeName := c.Param("type")
|
typeName := c.Param("type")
|
||||||
|
|
||||||
_, ok := video.Types[typeName]
|
videoType, ok := video.Types[typeName]
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Errorf("video: invalid type %s", txt.Quote(typeName))
|
log.Errorf("video: invalid type %s", txt.Quote(typeName))
|
||||||
|
@ -69,18 +70,21 @@ func GetVideo(router *gin.RouterGroup) {
|
||||||
logError("video", f.Update("FileMissing", true))
|
logError("video", f.Update("FileMissing", true))
|
||||||
|
|
||||||
return
|
return
|
||||||
} else if !mf.IsPlayableVideo() {
|
} else if f.FileCodec != string(videoType.Codec) {
|
||||||
conv := service.Convert()
|
log.Debugf("video: transcoding %s from %s to avc", txt.Quote(f.FileName), txt.Quote(f.FileCodec))
|
||||||
avcFile, err := conv.ToAvc1(mf)
|
|
||||||
|
|
||||||
if err != nil {
|
start := time.Now()
|
||||||
|
conv := service.Convert()
|
||||||
|
|
||||||
|
if avcFile, err := conv.ToAvc(mf); err != nil {
|
||||||
log.Errorf("video: failed transcoding %s", txt.Quote(f.FileName))
|
log.Errorf("video: failed transcoding %s", txt.Quote(f.FileName))
|
||||||
c.Data(http.StatusOK, "image/svg+xml", videoIconSvg)
|
c.Data(http.StatusOK, "image/svg+xml", videoIconSvg)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
} else {
|
||||||
|
fileName = avcFile.FileName()
|
||||||
}
|
}
|
||||||
|
|
||||||
fileName = avcFile.FileName()
|
log.Debugf("video: transcoding completed in %s", time.Since(start))
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Query("download") != "" {
|
if c.Query("download") != "" {
|
||||||
|
|
|
@ -305,7 +305,7 @@ func (m *File) RelatedPhoto() *Photo {
|
||||||
|
|
||||||
// NoJPEG returns true if the file is not a JPEG image file.
|
// NoJPEG returns true if the file is not a JPEG image file.
|
||||||
func (m *File) NoJPEG() bool {
|
func (m *File) NoJPEG() bool {
|
||||||
return m.FileType != string(fs.TypeJpeg)
|
return m.FileType != string(fs.FormatJpeg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Links returns all share links for this entity.
|
// Links returns all share links for this entity.
|
||||||
|
|
|
@ -6,7 +6,7 @@ const CodecAvc1 = "avc1"
|
||||||
const CodecHeic = "heic"
|
const CodecHeic = "heic"
|
||||||
const CodecXMP = "xmp"
|
const CodecXMP = "xmp"
|
||||||
|
|
||||||
// CodecAvc1 returns true if the video is encoded with H.264/AVC
|
// CodecAvc returns true if the video format is MPEG-4 AVC.
|
||||||
func (data Data) CodecAvc1() bool {
|
func (data Data) CodecAvc() bool {
|
||||||
return data.Codec == CodecAvc1
|
return data.Codec == CodecAvc1
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ func TestData_CodecAvc1(t *testing.T) {
|
||||||
Codec: "avc1",
|
Codec: "avc1",
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, true, data.CodecAvc1())
|
assert.Equal(t, true, data.CodecAvc())
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("false", func(t *testing.T) {
|
t.Run("false", func(t *testing.T) {
|
||||||
|
@ -20,6 +20,6 @@ func TestData_CodecAvc1(t *testing.T) {
|
||||||
Codec: "heic",
|
Codec: "heic",
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, false, data.CodecAvc1())
|
assert.Equal(t, false, data.CodecAvc())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,14 +38,14 @@ func ValidDateTime(s string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exif parses an image file for Exif meta data and returns as Data struct.
|
// Exif parses an image file for Exif meta data and returns as Data struct.
|
||||||
func Exif(fileName string, fileType fs.FileType) (data Data, err error) {
|
func Exif(fileName string, fileType fs.FileFormat) (data Data, err error) {
|
||||||
err = data.Exif(fileName, fileType)
|
err = data.Exif(fileName, fileType)
|
||||||
|
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exif parses an image file for Exif meta data and returns as Data struct.
|
// Exif parses an image file for Exif meta data and returns as Data struct.
|
||||||
func (data *Data) Exif(fileName string, fileType fs.FileType) (err error) {
|
func (data *Data) Exif(fileName string, fileType fs.FileFormat) (err error) {
|
||||||
exifMutex.Lock()
|
exifMutex.Lock()
|
||||||
defer exifMutex.Unlock()
|
defer exifMutex.Unlock()
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ import (
|
||||||
"github.com/photoprism/photoprism/pkg/txt"
|
"github.com/photoprism/photoprism/pkg/txt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RawExif(fileName string, fileType fs.FileType) (rawExif []byte, err error) {
|
func RawExif(fileName string, fileType fs.FileFormat) (rawExif []byte, err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if e := recover(); e != nil {
|
if e := recover(); e != nil {
|
||||||
err = fmt.Errorf("metadata: %s in %s (raw exif panic)\nstack: %s", e, txt.Quote(filepath.Base(fileName)), debug.Stack())
|
err = fmt.Errorf("metadata: %s in %s (raw exif panic)\nstack: %s", e, txt.Quote(filepath.Base(fileName)), debug.Stack())
|
||||||
|
@ -27,7 +27,7 @@ func RawExif(fileName string, fileType fs.FileType) (rawExif []byte, err error)
|
||||||
|
|
||||||
logName := txt.Quote(filepath.Base(fileName))
|
logName := txt.Quote(filepath.Base(fileName))
|
||||||
|
|
||||||
if fileType == fs.TypeJpeg {
|
if fileType == fs.FormatJpeg {
|
||||||
jpegMp := jpegstructure.NewJpegMediaParser()
|
jpegMp := jpegstructure.NewJpegMediaParser()
|
||||||
|
|
||||||
sl, err := jpegMp.ParseFile(fileName)
|
sl, err := jpegMp.ParseFile(fileName)
|
||||||
|
@ -49,7 +49,7 @@ func RawExif(fileName string, fileType fs.FileType) (rawExif []byte, err error)
|
||||||
parsed = true
|
parsed = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if fileType == fs.TypePng {
|
} else if fileType == fs.FormatPng {
|
||||||
pngMp := pngstructure.NewPngMediaParser()
|
pngMp := pngstructure.NewPngMediaParser()
|
||||||
|
|
||||||
cs, err := pngMp.ParseFile(fileName)
|
cs, err := pngMp.ParseFile(fileName)
|
||||||
|
@ -69,7 +69,7 @@ func RawExif(fileName string, fileType fs.FileType) (rawExif []byte, err error)
|
||||||
} else {
|
} else {
|
||||||
parsed = true
|
parsed = true
|
||||||
}
|
}
|
||||||
} else if fileType == fs.TypeHEIF {
|
} else if fileType == fs.FormatHEIF {
|
||||||
heicMp := heicexif.NewHeicExifMediaParser()
|
heicMp := heicexif.NewHeicExifMediaParser()
|
||||||
|
|
||||||
cs, err := heicMp.ParseFile(fileName)
|
cs, err := heicMp.ParseFile(fileName)
|
||||||
|
@ -89,7 +89,7 @@ func RawExif(fileName string, fileType fs.FileType) (rawExif []byte, err error)
|
||||||
} else {
|
} else {
|
||||||
parsed = true
|
parsed = true
|
||||||
}
|
}
|
||||||
} else if fileType == fs.TypeTiff {
|
} else if fileType == fs.FormatTiff {
|
||||||
tiffMp := tiffstructure.NewTiffMediaParser()
|
tiffMp := tiffstructure.NewTiffMediaParser()
|
||||||
|
|
||||||
cs, err := tiffMp.ParseFile(fileName)
|
cs, err := tiffMp.ParseFile(fileName)
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
|
|
||||||
func TestExif(t *testing.T) {
|
func TestExif(t *testing.T) {
|
||||||
t.Run("photoshop.jpg", func(t *testing.T) {
|
t.Run("photoshop.jpg", func(t *testing.T) {
|
||||||
data, err := Exif("testdata/photoshop.jpg", fs.TypeJpeg)
|
data, err := Exif("testdata/photoshop.jpg", fs.FormatJpeg)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -40,7 +40,7 @@ func TestExif(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("ladybug.jpg", func(t *testing.T) {
|
t.Run("ladybug.jpg", func(t *testing.T) {
|
||||||
data, err := Exif("testdata/ladybug.jpg", fs.TypeJpeg)
|
data, err := Exif("testdata/ladybug.jpg", fs.FormatJpeg)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -72,7 +72,7 @@ func TestExif(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("gopro_hd2.jpg", func(t *testing.T) {
|
t.Run("gopro_hd2.jpg", func(t *testing.T) {
|
||||||
data, err := Exif("testdata/gopro_hd2.jpg", fs.TypeJpeg)
|
data, err := Exif("testdata/gopro_hd2.jpg", fs.FormatJpeg)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -101,7 +101,7 @@ func TestExif(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("tweethog.png", func(t *testing.T) {
|
t.Run("tweethog.png", func(t *testing.T) {
|
||||||
_, err := Exif("testdata/tweethog.png", fs.TypePng)
|
_, err := Exif("testdata/tweethog.png", fs.FormatPng)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("err should NOT be nil")
|
t.Fatal("err should NOT be nil")
|
||||||
|
@ -111,7 +111,7 @@ func TestExif(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("iphone_7.heic", func(t *testing.T) {
|
t.Run("iphone_7.heic", func(t *testing.T) {
|
||||||
data, err := Exif("testdata/iphone_7.heic", fs.TypeHEIF)
|
data, err := Exif("testdata/iphone_7.heic", fs.FormatHEIF)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -132,7 +132,7 @@ func TestExif(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("gps-2000.jpg", func(t *testing.T) {
|
t.Run("gps-2000.jpg", func(t *testing.T) {
|
||||||
data, err := Exif("testdata/gps-2000.jpg", fs.TypeJpeg)
|
data, err := Exif("testdata/gps-2000.jpg", fs.FormatJpeg)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -160,7 +160,7 @@ func TestExif(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("image-2011.jpg", func(t *testing.T) {
|
t.Run("image-2011.jpg", func(t *testing.T) {
|
||||||
data, err := Exif("testdata/image-2011.jpg", fs.TypeJpeg)
|
data, err := Exif("testdata/image-2011.jpg", fs.FormatJpeg)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -195,7 +195,7 @@ func TestExif(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("ship.jpg", func(t *testing.T) {
|
t.Run("ship.jpg", func(t *testing.T) {
|
||||||
data, err := Exif("testdata/ship.jpg", fs.TypeJpeg)
|
data, err := Exif("testdata/ship.jpg", fs.FormatJpeg)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -216,7 +216,7 @@ func TestExif(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("no-exif-data.jpg", func(t *testing.T) {
|
t.Run("no-exif-data.jpg", func(t *testing.T) {
|
||||||
_, err := Exif("testdata/no-exif-data.jpg", fs.TypeJpeg)
|
_, err := Exif("testdata/no-exif-data.jpg", fs.FormatJpeg)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("err should NOT be nil")
|
t.Fatal("err should NOT be nil")
|
||||||
|
@ -226,7 +226,7 @@ func TestExif(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("screenshot.png", func(t *testing.T) {
|
t.Run("screenshot.png", func(t *testing.T) {
|
||||||
data, err := Exif("testdata/screenshot.png", fs.TypePng)
|
data, err := Exif("testdata/screenshot.png", fs.FormatPng)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -237,7 +237,7 @@ func TestExif(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("orientation.jpg", func(t *testing.T) {
|
t.Run("orientation.jpg", func(t *testing.T) {
|
||||||
data, err := Exif("testdata/orientation.jpg", fs.TypeJpeg)
|
data, err := Exif("testdata/orientation.jpg", fs.FormatJpeg)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -265,13 +265,13 @@ func TestExif(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("gopher-preview.jpg", func(t *testing.T) {
|
t.Run("gopher-preview.jpg", func(t *testing.T) {
|
||||||
_, err := Exif("testdata/gopher-preview.jpg", fs.TypeJpeg)
|
_, err := Exif("testdata/gopher-preview.jpg", fs.FormatJpeg)
|
||||||
|
|
||||||
assert.EqualError(t, err, "metadata: no exif header in gopher-preview.jpg (search and extract)")
|
assert.EqualError(t, err, "metadata: no exif header in gopher-preview.jpg (search and extract)")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("huawei-gps-error.jpg", func(t *testing.T) {
|
t.Run("huawei-gps-error.jpg", func(t *testing.T) {
|
||||||
data, err := Exif("testdata/huawei-gps-error.jpg", fs.TypeJpeg)
|
data, err := Exif("testdata/huawei-gps-error.jpg", fs.FormatJpeg)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -292,7 +292,7 @@ func TestExif(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("panorama360.jpg", func(t *testing.T) {
|
t.Run("panorama360.jpg", func(t *testing.T) {
|
||||||
data, err := Exif("testdata/panorama360.jpg", fs.TypeJpeg)
|
data, err := Exif("testdata/panorama360.jpg", fs.FormatJpeg)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -323,7 +323,7 @@ func TestExif(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("exif-example.tiff", func(t *testing.T) {
|
t.Run("exif-example.tiff", func(t *testing.T) {
|
||||||
data, err := Exif("testdata/exif-example.tiff", fs.TypeTiff)
|
data, err := Exif("testdata/exif-example.tiff", fs.FormatTiff)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -354,7 +354,7 @@ func TestExif(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("out-of-range-500.jpg", func(t *testing.T) {
|
t.Run("out-of-range-500.jpg", func(t *testing.T) {
|
||||||
data, err := Exif("testdata/out-of-range-500.jpg", fs.TypeJpeg)
|
data, err := Exif("testdata/out-of-range-500.jpg", fs.FormatJpeg)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -385,7 +385,7 @@ func TestExif(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("digikam.jpg", func(t *testing.T) {
|
t.Run("digikam.jpg", func(t *testing.T) {
|
||||||
data, err := Exif("testdata/digikam.jpg", fs.TypeJpeg)
|
data, err := Exif("testdata/digikam.jpg", fs.FormatJpeg)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -419,7 +419,7 @@ func TestExif(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("notebook.jpg", func(t *testing.T) {
|
t.Run("notebook.jpg", func(t *testing.T) {
|
||||||
data, err := Exif("testdata/notebook.jpg", fs.TypeJpeg)
|
data, err := Exif("testdata/notebook.jpg", fs.FormatJpeg)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|
|
@ -115,7 +115,7 @@ func (c *Convert) Start(path string) error {
|
||||||
|
|
||||||
// ToJson uses exiftool to export metadata to a json file.
|
// ToJson uses exiftool to export metadata to a json file.
|
||||||
func (c *Convert) ToJson(mf *MediaFile) (*MediaFile, error) {
|
func (c *Convert) ToJson(mf *MediaFile) (*MediaFile, error) {
|
||||||
jsonName := fs.TypeJson.FindFirst(mf.FileName(), []string{c.conf.SidecarPath(), fs.HiddenPath}, c.conf.OriginalsPath(), false)
|
jsonName := fs.FormatJson.FindFirst(mf.FileName(), []string{c.conf.SidecarPath(), fs.HiddenPath}, c.conf.OriginalsPath(), false)
|
||||||
|
|
||||||
result, err := NewMediaFile(jsonName)
|
result, err := NewMediaFile(jsonName)
|
||||||
|
|
||||||
|
@ -220,7 +220,7 @@ func (c *Convert) ToJpeg(image *MediaFile) (*MediaFile, error) {
|
||||||
return image, nil
|
return image, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
jpegName := fs.TypeJpeg.FindFirst(image.FileName(), []string{c.conf.SidecarPath(), fs.HiddenPath}, c.conf.OriginalsPath(), false)
|
jpegName := fs.FormatJpeg.FindFirst(image.FileName(), []string{c.conf.SidecarPath(), fs.HiddenPath}, c.conf.OriginalsPath(), false)
|
||||||
|
|
||||||
mediaFile, err := NewMediaFile(jpegName)
|
mediaFile, err := NewMediaFile(jpegName)
|
||||||
|
|
||||||
|
@ -237,7 +237,7 @@ func (c *Convert) ToJpeg(image *MediaFile) (*MediaFile, error) {
|
||||||
|
|
||||||
log.Debugf("convert: %s -> %s", fileName, filepath.Base(jpegName))
|
log.Debugf("convert: %s -> %s", fileName, filepath.Base(jpegName))
|
||||||
|
|
||||||
xmpName := fs.TypeXMP.Find(image.FileName(), false)
|
xmpName := fs.FormatXMP.Find(image.FileName(), false)
|
||||||
|
|
||||||
event.Publish("index.converting", event.Data{
|
event.Publish("index.converting", event.Data{
|
||||||
"fileType": image.FileType(),
|
"fileType": image.FileType(),
|
||||||
|
@ -287,10 +287,18 @@ func (c *Convert) ToJpeg(image *MediaFile) (*MediaFile, error) {
|
||||||
return NewMediaFile(jpegName)
|
return NewMediaFile(jpegName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AvcConvertCommand returns the command for converting video files to AVC1.
|
// AvcConvertCommand returns the command for converting video files to MPEG-4 AVC.
|
||||||
func (c *Convert) AvcConvertCommand(mf *MediaFile, avcName string) (result *exec.Cmd, useMutex bool, err error) {
|
func (c *Convert) AvcConvertCommand(mf *MediaFile, avcName string) (result *exec.Cmd, useMutex bool, err error) {
|
||||||
if mf.IsVideo() {
|
if mf.IsVideo() {
|
||||||
result = exec.Command(c.conf.FFmpegBin(), "-i", mf.FileName(), avcName)
|
// Don't transcode more than one video at the same time.
|
||||||
|
useMutex = true
|
||||||
|
result = exec.Command(
|
||||||
|
c.conf.FFmpegBin(),
|
||||||
|
"-i", mf.FileName(),
|
||||||
|
"-c:v", "libx264",
|
||||||
|
"-f", "mp4",
|
||||||
|
avcName,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
return nil, useMutex, fmt.Errorf("convert: file type %s not supported in %s", mf.FileType(), txt.Quote(mf.BaseName()))
|
return nil, useMutex, fmt.Errorf("convert: file type %s not supported in %s", mf.FileType(), txt.Quote(mf.BaseName()))
|
||||||
}
|
}
|
||||||
|
@ -298,21 +306,17 @@ func (c *Convert) AvcConvertCommand(mf *MediaFile, avcName string) (result *exec
|
||||||
return result, useMutex, nil
|
return result, useMutex, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToAvc1 converts a single video file to AVC1 if possible.
|
// ToAvc converts a single video file to MPEG-4 AVC.
|
||||||
func (c *Convert) ToAvc1(video *MediaFile) (*MediaFile, error) {
|
func (c *Convert) ToAvc(video *MediaFile) (*MediaFile, error) {
|
||||||
if !video.Exists() {
|
if !video.Exists() {
|
||||||
return nil, fmt.Errorf("convert: can not convert to avc1, file does not exist (%s)", video.RelName(c.conf.OriginalsPath()))
|
return nil, fmt.Errorf("convert: can not convert to avc1, file does not exist (%s)", video.RelName(c.conf.OriginalsPath()))
|
||||||
}
|
}
|
||||||
|
|
||||||
if video.IsPlayableVideo() {
|
avcName := fs.FormatAvc.FindFirst(video.FileName(), []string{c.conf.SidecarPath(), fs.HiddenPath}, c.conf.OriginalsPath(), false)
|
||||||
return video, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
avcName := fs.TypeMp4.FindFirst(video.FileName(), []string{c.conf.SidecarPath(), fs.HiddenPath}, c.conf.OriginalsPath(), false)
|
|
||||||
|
|
||||||
mediaFile, err := NewMediaFile(avcName)
|
mediaFile, err := NewMediaFile(avcName)
|
||||||
|
|
||||||
if err == nil && mediaFile.IsPlayableVideo() {
|
if err == nil && mediaFile.IsVideo() {
|
||||||
return mediaFile, nil
|
return mediaFile, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -335,6 +339,7 @@ func (c *Convert) ToAvc1(video *MediaFile) (*MediaFile, error) {
|
||||||
cmd, useMutex, err := c.AvcConvertCommand(video, avcName)
|
cmd, useMutex, err := c.AvcConvertCommand(video, avcName)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -232,7 +232,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
||||||
photo.PhotoQuality = -1
|
photo.PhotoQuality = -1
|
||||||
photo.PhotoSingle = o.Single
|
photo.PhotoSingle = o.Single
|
||||||
|
|
||||||
if yamlName := fs.TypeYaml.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), stripSequence); yamlName != "" {
|
if yamlName := fs.FormatYaml.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), stripSequence); yamlName != "" {
|
||||||
if err := photo.LoadFromYaml(yamlName); err != nil {
|
if err := photo.LoadFromYaml(yamlName); err != nil {
|
||||||
log.Errorf("index: %s in %s (restore from yaml)", err.Error(), logName)
|
log.Errorf("index: %s in %s (restore from yaml)", err.Error(), logName)
|
||||||
} else if err := photo.Find(); err != nil {
|
} else if err := photo.Find(); err != nil {
|
||||||
|
|
|
@ -31,7 +31,7 @@ type MediaFile struct {
|
||||||
statErr error
|
statErr error
|
||||||
modTime time.Time
|
modTime time.Time
|
||||||
fileSize int64
|
fileSize int64
|
||||||
fileType fs.FileType
|
fileType fs.FileFormat
|
||||||
mimeType string
|
mimeType string
|
||||||
takenAt time.Time
|
takenAt time.Time
|
||||||
takenAtSrc string
|
takenAtSrc string
|
||||||
|
@ -50,7 +50,7 @@ func NewMediaFile(fileName string) (*MediaFile, error) {
|
||||||
m := &MediaFile{
|
m := &MediaFile{
|
||||||
fileName: fileName,
|
fileName: fileName,
|
||||||
fileRoot: entity.RootUnknown,
|
fileRoot: entity.RootUnknown,
|
||||||
fileType: fs.TypeOther,
|
fileType: fs.FormatOther,
|
||||||
metaData: meta.NewData(),
|
metaData: meta.NewData(),
|
||||||
width: -1,
|
width: -1,
|
||||||
height: -1,
|
height: -1,
|
||||||
|
@ -348,7 +348,7 @@ func (m *MediaFile) RelatedFiles(stripSequence bool) (result RelatedFiles, err e
|
||||||
|
|
||||||
// Add hidden JPEG if exists.
|
// Add hidden JPEG if exists.
|
||||||
if !result.ContainsJpeg() {
|
if !result.ContainsJpeg() {
|
||||||
if jpegName := fs.TypeJpeg.FindFirst(result.Main.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), stripSequence); jpegName != "" {
|
if jpegName := fs.FormatJpeg.FindFirst(result.Main.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), stripSequence); jpegName != "" {
|
||||||
if resultFile, err := NewMediaFile(jpegName); err == nil {
|
if resultFile, err := NewMediaFile(jpegName); err == nil {
|
||||||
result.Files = append(result.Files, resultFile)
|
result.Files = append(result.Files, resultFile)
|
||||||
}
|
}
|
||||||
|
@ -654,7 +654,7 @@ func (m *MediaFile) IsGif() bool {
|
||||||
|
|
||||||
// IsTiff returns true if this is a TIFF file.
|
// IsTiff returns true if this is a TIFF file.
|
||||||
func (m *MediaFile) IsTiff() bool {
|
func (m *MediaFile) IsTiff() bool {
|
||||||
return m.HasFileType(fs.TypeTiff) && m.MimeType() == fs.MimeTypeTiff
|
return m.HasFileType(fs.FormatTiff) && m.MimeType() == fs.MimeTypeTiff
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsHEIF returns true if this is a High Efficiency Image File Format file.
|
// IsHEIF returns true if this is a High Efficiency Image File Format file.
|
||||||
|
@ -674,24 +674,24 @@ func (m *MediaFile) IsVideo() bool {
|
||||||
|
|
||||||
// IsJson return true if this media file is a json sidecar file.
|
// IsJson return true if this media file is a json sidecar file.
|
||||||
func (m *MediaFile) IsJson() bool {
|
func (m *MediaFile) IsJson() bool {
|
||||||
return m.HasFileType(fs.TypeJson)
|
return m.HasFileType(fs.FormatJson)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FileType returns the file type (jpg, gif, tiff,...).
|
// FileType returns the file type (jpg, gif, tiff,...).
|
||||||
func (m *MediaFile) FileType() fs.FileType {
|
func (m *MediaFile) FileType() fs.FileFormat {
|
||||||
switch {
|
switch {
|
||||||
case m.IsJpeg():
|
case m.IsJpeg():
|
||||||
return fs.TypeJpeg
|
return fs.FormatJpeg
|
||||||
case m.IsPng():
|
case m.IsPng():
|
||||||
return fs.TypePng
|
return fs.FormatPng
|
||||||
case m.IsGif():
|
case m.IsGif():
|
||||||
return fs.TypeGif
|
return fs.FormatGif
|
||||||
case m.IsHEIF():
|
case m.IsHEIF():
|
||||||
return fs.TypeHEIF
|
return fs.FormatHEIF
|
||||||
case m.IsBitmap():
|
case m.IsBitmap():
|
||||||
return fs.TypeBitmap
|
return fs.FormatBitmap
|
||||||
default:
|
default:
|
||||||
return fs.GetFileType(m.fileName)
|
return fs.GetFileFormat(m.fileName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -701,8 +701,8 @@ func (m *MediaFile) MediaType() fs.MediaType {
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasFileType returns true if this is the given type.
|
// HasFileType returns true if this is the given type.
|
||||||
func (m *MediaFile) HasFileType(fileType fs.FileType) bool {
|
func (m *MediaFile) HasFileType(fileType fs.FileFormat) bool {
|
||||||
if fileType == fs.TypeJpeg {
|
if fileType == fs.FormatJpeg {
|
||||||
return m.IsJpeg()
|
return m.IsJpeg()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -711,7 +711,7 @@ func (m *MediaFile) HasFileType(fileType fs.FileType) bool {
|
||||||
|
|
||||||
// IsRaw returns true if this is a RAW file.
|
// IsRaw returns true if this is a RAW file.
|
||||||
func (m *MediaFile) IsRaw() bool {
|
func (m *MediaFile) IsRaw() bool {
|
||||||
return m.HasFileType(fs.TypeRaw)
|
return m.HasFileType(fs.FormatRaw)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsImageOther returns true if this is a PNG, GIF, BMP or TIFF file.
|
// IsImageOther returns true if this is a PNG, GIF, BMP or TIFF file.
|
||||||
|
@ -726,7 +726,7 @@ func (m *MediaFile) IsImageOther() bool {
|
||||||
|
|
||||||
// IsXMP returns true if this is a XMP sidecar file.
|
// IsXMP returns true if this is a XMP sidecar file.
|
||||||
func (m *MediaFile) IsXMP() bool {
|
func (m *MediaFile) IsXMP() bool {
|
||||||
return m.FileType() == fs.TypeXMP
|
return m.FileType() == fs.FormatXMP
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsSidecar returns true if this is a sidecar file (containing metadata).
|
// IsSidecar returns true if this is a sidecar file (containing metadata).
|
||||||
|
@ -736,7 +736,7 @@ func (m *MediaFile) IsSidecar() bool {
|
||||||
|
|
||||||
// IsPlayableVideo returns true if this is a supported video file format.
|
// IsPlayableVideo returns true if this is a supported video file format.
|
||||||
func (m *MediaFile) IsPlayableVideo() bool {
|
func (m *MediaFile) IsPlayableVideo() bool {
|
||||||
return m.IsVideo() && m.HasFileType(fs.TypeMp4)
|
return m.IsVideo() && (m.HasFileType(fs.FormatMp4) || m.HasFileType(fs.FormatAvc))
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsPhoto returns true if this file is a photo / image.
|
// IsPhoto returns true if this file is a photo / image.
|
||||||
|
@ -764,7 +764,7 @@ func (m *MediaFile) Jpeg() (*MediaFile, error) {
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
jpegFilename := fs.TypeJpeg.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false)
|
jpegFilename := fs.FormatJpeg.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false)
|
||||||
|
|
||||||
if jpegFilename == "" {
|
if jpegFilename == "" {
|
||||||
return nil, fmt.Errorf("no jpeg found for %s", m.FileName())
|
return nil, fmt.Errorf("no jpeg found for %s", m.FileName())
|
||||||
|
@ -784,7 +784,7 @@ func (m *MediaFile) HasJpeg() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
jpegName := fs.TypeJpeg.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false)
|
jpegName := fs.FormatJpeg.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false)
|
||||||
|
|
||||||
if jpegName == "" {
|
if jpegName == "" {
|
||||||
m.hasJpeg = false
|
m.hasJpeg = false
|
||||||
|
@ -801,7 +801,7 @@ func (m *MediaFile) HasJson() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return fs.TypeJson.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false) != ""
|
return fs.FormatJson.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false) != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MediaFile) decodeDimensions() error {
|
func (m *MediaFile) decodeDimensions() error {
|
||||||
|
|
|
@ -1151,7 +1151,7 @@ func TestMediaFile_IsPng(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, fs.TypePng, mediaFile.FileType())
|
assert.Equal(t, fs.FormatPng, mediaFile.FileType())
|
||||||
assert.Equal(t, "image/png", mediaFile.MimeType())
|
assert.Equal(t, "image/png", mediaFile.MimeType())
|
||||||
assert.Equal(t, true, mediaFile.IsPng())
|
assert.Equal(t, true, mediaFile.IsPng())
|
||||||
})
|
})
|
||||||
|
@ -1165,7 +1165,7 @@ func TestMediaFile_IsTiff(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
assert.Equal(t, fs.TypeJson, mediaFile.FileType())
|
assert.Equal(t, fs.FormatJson, mediaFile.FileType())
|
||||||
assert.Equal(t, "", mediaFile.MimeType())
|
assert.Equal(t, "", mediaFile.MimeType())
|
||||||
assert.Equal(t, false, mediaFile.IsTiff())
|
assert.Equal(t, false, mediaFile.IsTiff())
|
||||||
})
|
})
|
||||||
|
@ -1176,7 +1176,7 @@ func TestMediaFile_IsTiff(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
assert.Equal(t, fs.TypeTiff, mediaFile.FileType())
|
assert.Equal(t, fs.FormatTiff, mediaFile.FileType())
|
||||||
assert.Equal(t, "image/tiff", mediaFile.MimeType())
|
assert.Equal(t, "image/tiff", mediaFile.MimeType())
|
||||||
assert.Equal(t, true, mediaFile.IsTiff())
|
assert.Equal(t, true, mediaFile.IsTiff())
|
||||||
})
|
})
|
||||||
|
@ -1187,7 +1187,7 @@ func TestMediaFile_IsTiff(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
assert.Equal(t, fs.TypeTiff, mediaFile.FileType())
|
assert.Equal(t, fs.FormatTiff, mediaFile.FileType())
|
||||||
assert.Equal(t, "image/tiff", mediaFile.MimeType())
|
assert.Equal(t, "image/tiff", mediaFile.MimeType())
|
||||||
assert.Equal(t, true, mediaFile.IsTiff())
|
assert.Equal(t, true, mediaFile.IsTiff())
|
||||||
})
|
})
|
||||||
|
@ -1228,7 +1228,7 @@ func TestMediaFile_IsImageOther(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
assert.Equal(t, fs.TypeBitmap, mediaFile.FileType())
|
assert.Equal(t, fs.FormatBitmap, mediaFile.FileType())
|
||||||
assert.Equal(t, "image/bmp", mediaFile.MimeType())
|
assert.Equal(t, "image/bmp", mediaFile.MimeType())
|
||||||
assert.Equal(t, true, mediaFile.IsBitmap())
|
assert.Equal(t, true, mediaFile.IsBitmap())
|
||||||
assert.Equal(t, true, mediaFile.IsImageOther())
|
assert.Equal(t, true, mediaFile.IsImageOther())
|
||||||
|
@ -1241,7 +1241,7 @@ func TestMediaFile_IsImageOther(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, fs.TypeGif, mediaFile.FileType())
|
assert.Equal(t, fs.FormatGif, mediaFile.FileType())
|
||||||
assert.Equal(t, "image/gif", mediaFile.MimeType())
|
assert.Equal(t, "image/gif", mediaFile.MimeType())
|
||||||
assert.Equal(t, true, mediaFile.IsImageOther())
|
assert.Equal(t, true, mediaFile.IsImageOther())
|
||||||
})
|
})
|
||||||
|
@ -1837,7 +1837,7 @@ func TestMediaFile_FileType(t *testing.T) {
|
||||||
|
|
||||||
assert.True(t, m.IsJpeg())
|
assert.True(t, m.IsJpeg())
|
||||||
assert.Equal(t, "jpg", string(m.FileType()))
|
assert.Equal(t, "jpg", string(m.FileType()))
|
||||||
assert.Equal(t, fs.TypeJpeg, m.FileType())
|
assert.Equal(t, fs.FormatJpeg, m.FileType())
|
||||||
assert.Equal(t, ".png", m.Extension())
|
assert.Equal(t, ".png", m.Extension())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ func (m *MediaFile) MetaData() (result meta.Data) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse regular JSON sidecar files ("img_1234.json").
|
// Parse regular JSON sidecar files ("img_1234.json").
|
||||||
if jsonFiles := fs.TypeJson.FindAll(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false); len(jsonFiles) == 0 {
|
if jsonFiles := fs.FormatJson.FindAll(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false); len(jsonFiles) == 0 {
|
||||||
log.Debugf("media: no json sidecar file found for %s", txt.Quote(filepath.Base(m.FileName())))
|
log.Debugf("media: no json sidecar file found for %s", txt.Quote(filepath.Base(m.FileName())))
|
||||||
} else {
|
} else {
|
||||||
for _, jsonFile := range jsonFiles {
|
for _, jsonFile := range jsonFiles {
|
||||||
|
|
|
@ -11,7 +11,7 @@ func AccountUploads(a entity.Account, limit int) (results entity.Files, err erro
|
||||||
Where("files.id NOT IN (SELECT file_id FROM files_sync WHERE file_id > 0 AND account_id = ?)", a.ID)
|
Where("files.id NOT IN (SELECT file_id FROM files_sync WHERE file_id > 0 AND account_id = ?)", a.ID)
|
||||||
|
|
||||||
if !a.SyncRaw {
|
if !a.SyncRaw {
|
||||||
s = s.Where("files.file_type <> ? OR files.file_type IS NULL", fs.TypeRaw)
|
s = s.Where("files.file_type <> ? OR files.file_type IS NULL", fs.FormatRaw)
|
||||||
}
|
}
|
||||||
|
|
||||||
s = s.Order("files.file_name ASC")
|
s = s.Order("files.file_name ASC")
|
||||||
|
|
|
@ -15,15 +15,15 @@ import (
|
||||||
"github.com/disintegration/imaging"
|
"github.com/disintegration/imaging"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ResampleOptions(opts ...ResampleOption) (method ResampleOption, filter imaging.ResampleFilter, format fs.FileType) {
|
func ResampleOptions(opts ...ResampleOption) (method ResampleOption, filter imaging.ResampleFilter, format fs.FileFormat) {
|
||||||
method = ResampleFit
|
method = ResampleFit
|
||||||
filter = imaging.Lanczos
|
filter = imaging.Lanczos
|
||||||
format = fs.TypeJpeg
|
format = fs.FormatJpeg
|
||||||
|
|
||||||
for _, option := range opts {
|
for _, option := range opts {
|
||||||
switch option {
|
switch option {
|
||||||
case ResamplePng:
|
case ResamplePng:
|
||||||
format = fs.TypePng
|
format = fs.FormatPng
|
||||||
case ResampleNearestNeighbor:
|
case ResampleNearestNeighbor:
|
||||||
filter = imaging.NearestNeighbor
|
filter = imaging.NearestNeighbor
|
||||||
case ResampleDefault:
|
case ResampleDefault:
|
||||||
|
@ -165,7 +165,7 @@ func Create(img image.Image, fileName string, width, height int, opts ...Resampl
|
||||||
|
|
||||||
var saveOption imaging.EncodeOption
|
var saveOption imaging.EncodeOption
|
||||||
|
|
||||||
if filepath.Ext(fileName) == "."+string(fs.TypePng) {
|
if filepath.Ext(fileName) == "."+string(fs.FormatPng) {
|
||||||
saveOption = imaging.PNGCompressionLevel(png.DefaultCompression)
|
saveOption = imaging.PNGCompressionLevel(png.DefaultCompression)
|
||||||
} else if width <= 150 && height <= 150 {
|
} else if width <= 150 && height <= 150 {
|
||||||
saveOption = imaging.JPEGQuality(JpegQualitySmall)
|
saveOption = imaging.JPEGQuality(JpegQualitySmall)
|
||||||
|
|
|
@ -15,21 +15,21 @@ func TestResampleOptions(t *testing.T) {
|
||||||
|
|
||||||
assert.Equal(t, ResampleFillCenter, method)
|
assert.Equal(t, ResampleFillCenter, method)
|
||||||
assert.Equal(t, imaging.Lanczos.Support, filter.Support)
|
assert.Equal(t, imaging.Lanczos.Support, filter.Support)
|
||||||
assert.Equal(t, fs.TypePng, format)
|
assert.Equal(t, fs.FormatPng, format)
|
||||||
})
|
})
|
||||||
t.Run("ResampleNearestNeighbor, FillTopLeft", func(t *testing.T) {
|
t.Run("ResampleNearestNeighbor, FillTopLeft", func(t *testing.T) {
|
||||||
method, filter, format := ResampleOptions(ResampleNearestNeighbor, ResampleFillTopLeft)
|
method, filter, format := ResampleOptions(ResampleNearestNeighbor, ResampleFillTopLeft)
|
||||||
|
|
||||||
assert.Equal(t, ResampleFillTopLeft, method)
|
assert.Equal(t, ResampleFillTopLeft, method)
|
||||||
assert.Equal(t, imaging.NearestNeighbor.Support, filter.Support)
|
assert.Equal(t, imaging.NearestNeighbor.Support, filter.Support)
|
||||||
assert.Equal(t, fs.TypeJpeg, format)
|
assert.Equal(t, fs.FormatJpeg, format)
|
||||||
})
|
})
|
||||||
t.Run("ResampleNearestNeighbor, FillBottomRight", func(t *testing.T) {
|
t.Run("ResampleNearestNeighbor, FillBottomRight", func(t *testing.T) {
|
||||||
method, filter, format := ResampleOptions(ResampleNearestNeighbor, ResampleFillBottomRight)
|
method, filter, format := ResampleOptions(ResampleNearestNeighbor, ResampleFillBottomRight)
|
||||||
|
|
||||||
assert.Equal(t, ResampleFillBottomRight, method)
|
assert.Equal(t, ResampleFillBottomRight, method)
|
||||||
assert.Equal(t, imaging.NearestNeighbor.Support, filter.Support)
|
assert.Equal(t, imaging.NearestNeighbor.Support, filter.Support)
|
||||||
assert.Equal(t, fs.TypeJpeg, format)
|
assert.Equal(t, fs.FormatJpeg, format)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Type struct {
|
type Type struct {
|
||||||
Format fs.FileType
|
Format fs.FileFormat
|
||||||
|
Codec fs.FileCodec
|
||||||
Width int
|
Width int
|
||||||
Height int
|
Height int
|
||||||
Public bool
|
Public bool
|
||||||
|
@ -44,14 +45,24 @@ type Type struct {
|
||||||
|
|
||||||
type TypeMap map[string]Type
|
type TypeMap map[string]Type
|
||||||
|
|
||||||
var TypeMP4 = Type{
|
var TypeMp4 = Type{
|
||||||
Format: fs.TypeMp4,
|
Format: fs.FormatMp4,
|
||||||
|
Codec: fs.CodecAvc,
|
||||||
|
Width: 0,
|
||||||
|
Height: 0,
|
||||||
|
Public: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
var TypeAvc = Type{
|
||||||
|
Format: fs.FormatAvc,
|
||||||
|
Codec: fs.CodecAvc,
|
||||||
Width: 0,
|
Width: 0,
|
||||||
Height: 0,
|
Height: 0,
|
||||||
Public: true,
|
Public: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
var Types = TypeMap{
|
var Types = TypeMap{
|
||||||
"": TypeMP4,
|
"": TypeAvc,
|
||||||
"mp4": TypeMP4,
|
"mp4": TypeMp4,
|
||||||
|
"avc": TypeAvc,
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,15 @@ package video
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
func TestTypes(t *testing.T) {
|
func TestTypes(t *testing.T) {
|
||||||
if val := Types[""]; val != TypeMP4 {
|
if val := Types[""]; val != TypeAvc {
|
||||||
t.Fatal("default type should be TypeMP4")
|
t.Fatal("default type should be avc")
|
||||||
}
|
}
|
||||||
|
|
||||||
if val := Types["mp4"]; val != TypeMP4 {
|
if val := Types["mp4"]; val != TypeMp4 {
|
||||||
t.Fatal("mp4 type should be TypeMP4")
|
t.Fatal("mp4 type should be mp4")
|
||||||
|
}
|
||||||
|
|
||||||
|
if val := Types["avc"]; val != TypeAvc {
|
||||||
|
t.Fatal("mp4 type should be avc")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
16
pkg/fs/codec.go
Normal file
16
pkg/fs/codec.go
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
package fs
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "image/gif" // Import for image.
|
||||||
|
_ "image/jpeg"
|
||||||
|
_ "image/png"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FileCodec string
|
||||||
|
|
||||||
|
const (
|
||||||
|
CodecAvc FileCodec = "avc1"
|
||||||
|
CodecHvc FileCodec = "hvc1"
|
||||||
|
CodecJpeg FileCodec = "jpeg"
|
||||||
|
CodecOther FileCodec = ""
|
||||||
|
)
|
|
@ -9,128 +9,129 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FileType string
|
type FileFormat string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TypeJpeg FileType = "jpg" // JPEG image file.
|
FormatJpeg FileFormat = "jpg" // JPEG image file.
|
||||||
TypePng FileType = "png" // PNG image file.
|
FormatPng FileFormat = "png" // PNG image file.
|
||||||
TypeGif FileType = "gif" // GIF image file.
|
FormatGif FileFormat = "gif" // GIF image file.
|
||||||
TypeTiff FileType = "tiff" // TIFF image file.
|
FormatTiff FileFormat = "tiff" // TIFF image file.
|
||||||
TypeBitmap FileType = "bmp" // BMP image file.
|
FormatBitmap FileFormat = "bmp" // BMP image file.
|
||||||
TypeRaw FileType = "raw" // RAW image file.
|
FormatRaw FileFormat = "raw" // RAW image file.
|
||||||
TypeHEIF FileType = "heif" // High Efficiency Image File Format
|
FormatHEIF FileFormat = "heif" // High Efficiency Image File Format
|
||||||
TypeMov FileType = "mov" // Video files.
|
FormatHEVC FileFormat = "hevc"
|
||||||
TypeMp4 FileType = "mp4"
|
FormatMov FileFormat = "mov" // Video files.
|
||||||
TypeHEVC FileType = "hevc"
|
FormatMp4 FileFormat = "mp4"
|
||||||
TypeAvi FileType = "avi"
|
FormatAvc FileFormat = "avc"
|
||||||
Type3gp FileType = "3gp"
|
FormatAvi FileFormat = "avi"
|
||||||
Type3g2 FileType = "3g2"
|
Format3gp FileFormat = "3gp"
|
||||||
TypeFlv FileType = "flv"
|
Format3g2 FileFormat = "3g2"
|
||||||
TypeMkv FileType = "mkv"
|
FormatFlv FileFormat = "flv"
|
||||||
TypeMpg FileType = "mpg"
|
FormatMkv FileFormat = "mkv"
|
||||||
TypeOgv FileType = "ogv"
|
FormatMpg FileFormat = "mpg"
|
||||||
TypeWebm FileType = "webm"
|
FormatOgv FileFormat = "ogv"
|
||||||
TypeWMV FileType = "wmv"
|
FormatWebm FileFormat = "webm"
|
||||||
TypeXMP FileType = "xmp" // Adobe XMP sidecar file (XML).
|
FormatWMV FileFormat = "wmv"
|
||||||
TypeAAE FileType = "aae" // Apple sidecar file (XML).
|
FormatXMP FileFormat = "xmp" // Adobe XMP sidecar file (XML).
|
||||||
TypeXML FileType = "xml" // XML metadata / config / sidecar file.
|
FormatAAE FileFormat = "aae" // Apple sidecar file (XML).
|
||||||
TypeYaml FileType = "yml" // YAML metadata / config / sidecar file.
|
FormatXML FileFormat = "xml" // XML metadata / config / sidecar file.
|
||||||
TypeToml FileType = "toml" // Tom's Obvious, Minimal Language sidecar file.
|
FormatYaml FileFormat = "yml" // YAML metadata / config / sidecar file.
|
||||||
TypeJson FileType = "json" // JSON metadata / config / sidecar file.
|
FormatToml FileFormat = "toml" // Tom's Obvious, Minimal Language sidecar file.
|
||||||
TypeText FileType = "txt" // Text config / sidecar file.
|
FormatJson FileFormat = "json" // JSON metadata / config / sidecar file.
|
||||||
TypeMarkdown FileType = "md" // Markdown text sidecar file.
|
FormatText FileFormat = "txt" // Text config / sidecar file.
|
||||||
TypeOther FileType = "" // Unknown file format.
|
FormatMarkdown FileFormat = "md" // Markdown text sidecar file.
|
||||||
|
FormatOther FileFormat = "" // Unknown file format.
|
||||||
)
|
)
|
||||||
|
|
||||||
type FileExtensions map[string]FileType
|
type FileExtensions map[string]FileFormat
|
||||||
type TypeExtensions map[FileType][]string
|
type TypeExtensions map[FileFormat][]string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
YamlExt = ".yml"
|
YamlExt = ".yml"
|
||||||
JpegExt = ".jpg"
|
JpegExt = ".jpg"
|
||||||
AvcExt = ".mp4"
|
AvcExt = ".avc"
|
||||||
HevcExt = ".hevc"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// FileExt contains the filename extensions of file formats known to PhotoPrism.
|
// FileExt contains the filename extensions of file formats known to PhotoPrism.
|
||||||
var FileExt = FileExtensions{
|
var FileExt = FileExtensions{
|
||||||
".bmp": TypeBitmap,
|
".bmp": FormatBitmap,
|
||||||
".gif": TypeGif,
|
".gif": FormatGif,
|
||||||
".tif": TypeTiff,
|
".tif": FormatTiff,
|
||||||
".tiff": TypeTiff,
|
".tiff": FormatTiff,
|
||||||
".png": TypePng,
|
".png": FormatPng,
|
||||||
".pn": TypePng,
|
".pn": FormatPng,
|
||||||
".crw": TypeRaw,
|
".crw": FormatRaw,
|
||||||
".cr2": TypeRaw,
|
".cr2": FormatRaw,
|
||||||
".cr3": TypeRaw,
|
".cr3": FormatRaw,
|
||||||
".nef": TypeRaw,
|
".nef": FormatRaw,
|
||||||
".arw": TypeRaw,
|
".arw": FormatRaw,
|
||||||
".dng": TypeRaw,
|
".dng": FormatRaw,
|
||||||
".mov": TypeMov,
|
".mov": FormatMov,
|
||||||
".avi": TypeAvi,
|
".avi": FormatAvi,
|
||||||
".mp4": TypeMp4,
|
".mp4": FormatMp4,
|
||||||
".hevc": TypeHEVC,
|
".avc": FormatAvc,
|
||||||
".3gp": Type3gp,
|
".hevc": FormatHEVC,
|
||||||
".3g2": Type3g2,
|
".3gp": Format3gp,
|
||||||
".flv": TypeFlv,
|
".3g2": Format3g2,
|
||||||
".mkv": TypeMkv,
|
".flv": FormatFlv,
|
||||||
".mpg": TypeMpg,
|
".mkv": FormatMkv,
|
||||||
".mpeg": TypeMpg,
|
".mpg": FormatMpg,
|
||||||
".ogv": TypeOgv,
|
".mpeg": FormatMpg,
|
||||||
".webm": TypeWebm,
|
".ogv": FormatOgv,
|
||||||
".wmv": TypeWMV,
|
".webm": FormatWebm,
|
||||||
".yml": TypeYaml,
|
".wmv": FormatWMV,
|
||||||
".yaml": TypeYaml,
|
".yml": FormatYaml,
|
||||||
".jpg": TypeJpeg,
|
".yaml": FormatYaml,
|
||||||
".jpeg": TypeJpeg,
|
".jpg": FormatJpeg,
|
||||||
".jpe": TypeJpeg,
|
".jpeg": FormatJpeg,
|
||||||
".jif": TypeJpeg,
|
".jpe": FormatJpeg,
|
||||||
".jfif": TypeJpeg,
|
".jif": FormatJpeg,
|
||||||
".jfi": TypeJpeg,
|
".jfif": FormatJpeg,
|
||||||
".thm": TypeJpeg,
|
".jfi": FormatJpeg,
|
||||||
".xmp": TypeXMP,
|
".thm": FormatJpeg,
|
||||||
".aae": TypeAAE,
|
".xmp": FormatXMP,
|
||||||
".heif": TypeHEIF,
|
".aae": FormatAAE,
|
||||||
".heic": TypeHEIF,
|
".heif": FormatHEIF,
|
||||||
".3fr": TypeRaw,
|
".heic": FormatHEIF,
|
||||||
".ari": TypeRaw,
|
".3fr": FormatRaw,
|
||||||
".bay": TypeRaw,
|
".ari": FormatRaw,
|
||||||
".cap": TypeRaw,
|
".bay": FormatRaw,
|
||||||
".data": TypeRaw,
|
".cap": FormatRaw,
|
||||||
".dcs": TypeRaw,
|
".data": FormatRaw,
|
||||||
".dcr": TypeRaw,
|
".dcs": FormatRaw,
|
||||||
".drf": TypeRaw,
|
".dcr": FormatRaw,
|
||||||
".eip": TypeRaw,
|
".drf": FormatRaw,
|
||||||
".erf": TypeRaw,
|
".eip": FormatRaw,
|
||||||
".fff": TypeRaw,
|
".erf": FormatRaw,
|
||||||
".gpr": TypeRaw,
|
".fff": FormatRaw,
|
||||||
".iiq": TypeRaw,
|
".gpr": FormatRaw,
|
||||||
".k25": TypeRaw,
|
".iiq": FormatRaw,
|
||||||
".kdc": TypeRaw,
|
".k25": FormatRaw,
|
||||||
".mdc": TypeRaw,
|
".kdc": FormatRaw,
|
||||||
".mef": TypeRaw,
|
".mdc": FormatRaw,
|
||||||
".mos": TypeRaw,
|
".mef": FormatRaw,
|
||||||
".mrw": TypeRaw,
|
".mos": FormatRaw,
|
||||||
".nrw": TypeRaw,
|
".mrw": FormatRaw,
|
||||||
".obm": TypeRaw,
|
".nrw": FormatRaw,
|
||||||
".orf": TypeRaw,
|
".obm": FormatRaw,
|
||||||
".pef": TypeRaw,
|
".orf": FormatRaw,
|
||||||
".ptx": TypeRaw,
|
".pef": FormatRaw,
|
||||||
".pxn": TypeRaw,
|
".ptx": FormatRaw,
|
||||||
".r3d": TypeRaw,
|
".pxn": FormatRaw,
|
||||||
".raf": TypeRaw,
|
".r3d": FormatRaw,
|
||||||
".raw": TypeRaw,
|
".raf": FormatRaw,
|
||||||
".rwl": TypeRaw,
|
".raw": FormatRaw,
|
||||||
".rw2": TypeRaw,
|
".rwl": FormatRaw,
|
||||||
".rwz": TypeRaw,
|
".rw2": FormatRaw,
|
||||||
".sr2": TypeRaw,
|
".rwz": FormatRaw,
|
||||||
".srf": TypeRaw,
|
".sr2": FormatRaw,
|
||||||
".srw": TypeRaw,
|
".srf": FormatRaw,
|
||||||
".x3f": TypeRaw,
|
".srw": FormatRaw,
|
||||||
".xml": TypeXML,
|
".x3f": FormatRaw,
|
||||||
".txt": TypeText,
|
".xml": FormatXML,
|
||||||
".md": TypeMarkdown,
|
".txt": FormatText,
|
||||||
".json": TypeJson,
|
".md": FormatMarkdown,
|
||||||
|
".json": FormatJson,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m FileExtensions) Known(name string) bool {
|
func (m FileExtensions) Known(name string) bool {
|
||||||
|
@ -169,7 +170,7 @@ func (m FileExtensions) TypeExt() TypeExtensions {
|
||||||
var TypeExt = FileExt.TypeExt()
|
var TypeExt = FileExt.TypeExt()
|
||||||
|
|
||||||
// Find returns the first filename with the same base name and a given type.
|
// Find returns the first filename with the same base name and a given type.
|
||||||
func (t FileType) Find(fileName string, stripSequence bool) string {
|
func (t FileFormat) Find(fileName string, stripSequence bool) string {
|
||||||
base := BasePrefix(fileName, stripSequence)
|
base := BasePrefix(fileName, stripSequence)
|
||||||
dir := filepath.Dir(fileName)
|
dir := filepath.Dir(fileName)
|
||||||
|
|
||||||
|
@ -194,20 +195,20 @@ func (t FileType) Find(fileName string, stripSequence bool) string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFileType returns the (expected) type for a given file name.
|
// GetFileFormat returns the (expected) type for a given file name.
|
||||||
func GetFileType(fileName string) FileType {
|
func GetFileFormat(fileName string) FileFormat {
|
||||||
fileExt := strings.ToLower(filepath.Ext(fileName))
|
fileExt := strings.ToLower(filepath.Ext(fileName))
|
||||||
result, ok := FileExt[fileExt]
|
result, ok := FileExt[fileExt]
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
result = TypeOther
|
result = FormatOther
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindFirst searches a list of directories for the first file with the same base name and a given type.
|
// FindFirst searches a list of directories for the first file with the same base name and a given type.
|
||||||
func (t FileType) FindFirst(fileName string, dirs []string, baseDir string, stripSequence bool) string {
|
func (t FileFormat) FindFirst(fileName string, dirs []string, baseDir string, stripSequence bool) string {
|
||||||
fileBase := filepath.Base(fileName)
|
fileBase := filepath.Base(fileName)
|
||||||
fileBasePrefix := BasePrefix(fileName, stripSequence)
|
fileBasePrefix := BasePrefix(fileName, stripSequence)
|
||||||
fileBaseLower := strings.ToLower(fileBasePrefix)
|
fileBaseLower := strings.ToLower(fileBasePrefix)
|
||||||
|
@ -256,7 +257,7 @@ func (t FileType) FindFirst(fileName string, dirs []string, baseDir string, stri
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindAll searches a list of directories for files with the same base name and a given type.
|
// FindAll searches a list of directories for files with the same base name and a given type.
|
||||||
func (t FileType) FindAll(fileName string, dirs []string, baseDir string, stripSequence bool) (results []string) {
|
func (t FileFormat) FindAll(fileName string, dirs []string, baseDir string, stripSequence bool) (results []string) {
|
||||||
fileBase := filepath.Base(fileName)
|
fileBase := filepath.Base(fileName)
|
||||||
fileBasePrefix := BasePrefix(fileName, stripSequence)
|
fileBasePrefix := BasePrefix(fileName, stripSequence)
|
||||||
fileBaseLower := strings.ToLower(fileBasePrefix)
|
fileBaseLower := strings.ToLower(fileBasePrefix)
|
||||||
|
|
|
@ -8,49 +8,49 @@ import (
|
||||||
|
|
||||||
func TestGetFileType(t *testing.T) {
|
func TestGetFileType(t *testing.T) {
|
||||||
t.Run("jpeg", func(t *testing.T) {
|
t.Run("jpeg", func(t *testing.T) {
|
||||||
result := GetFileType("testdata/test.jpg")
|
result := GetFileFormat("testdata/test.jpg")
|
||||||
assert.Equal(t, TypeJpeg, result)
|
assert.Equal(t, FormatJpeg, result)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("raw", func(t *testing.T) {
|
t.Run("raw", func(t *testing.T) {
|
||||||
result := GetFileType("testdata/test (jpg).CR2")
|
result := GetFileFormat("testdata/test (jpg).CR2")
|
||||||
assert.Equal(t, TypeRaw, result)
|
assert.Equal(t, FormatRaw, result)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("empty", func(t *testing.T) {
|
t.Run("empty", func(t *testing.T) {
|
||||||
result := GetFileType("")
|
result := GetFileFormat("")
|
||||||
assert.Equal(t, TypeOther, result)
|
assert.Equal(t, FormatOther, result)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFileType_Find(t *testing.T) {
|
func TestFileType_Find(t *testing.T) {
|
||||||
t.Run("find jpg", func(t *testing.T) {
|
t.Run("find jpg", func(t *testing.T) {
|
||||||
result := TypeJpeg.Find("testdata/test.xmp", false)
|
result := FormatJpeg.Find("testdata/test.xmp", false)
|
||||||
assert.Equal(t, "testdata/test.jpg", result)
|
assert.Equal(t, "testdata/test.jpg", result)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("upper ext", func(t *testing.T) {
|
t.Run("upper ext", func(t *testing.T) {
|
||||||
result := TypeJpeg.Find("testdata/test.XMP", false)
|
result := FormatJpeg.Find("testdata/test.XMP", false)
|
||||||
assert.Equal(t, "testdata/test.jpg", result)
|
assert.Equal(t, "testdata/test.jpg", result)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("with sequence", func(t *testing.T) {
|
t.Run("with sequence", func(t *testing.T) {
|
||||||
result := TypeJpeg.Find("testdata/test (2).xmp", false)
|
result := FormatJpeg.Find("testdata/test (2).xmp", false)
|
||||||
assert.Equal(t, "", result)
|
assert.Equal(t, "", result)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("strip sequence", func(t *testing.T) {
|
t.Run("strip sequence", func(t *testing.T) {
|
||||||
result := TypeJpeg.Find("testdata/test (2).xmp", true)
|
result := FormatJpeg.Find("testdata/test (2).xmp", true)
|
||||||
assert.Equal(t, "testdata/test.jpg", result)
|
assert.Equal(t, "testdata/test.jpg", result)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("name upper", func(t *testing.T) {
|
t.Run("name upper", func(t *testing.T) {
|
||||||
result := TypeJpeg.Find("testdata/CATYELLOW.xmp", true)
|
result := FormatJpeg.Find("testdata/CATYELLOW.xmp", true)
|
||||||
assert.Equal(t, "testdata/CATYELLOW.jpg", result)
|
assert.Equal(t, "testdata/CATYELLOW.jpg", result)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("name lower", func(t *testing.T) {
|
t.Run("name lower", func(t *testing.T) {
|
||||||
result := TypeJpeg.Find("testdata/chameleon_lime.xmp", true)
|
result := FormatJpeg.Find("testdata/chameleon_lime.xmp", true)
|
||||||
assert.Equal(t, "testdata/chameleon_lime.jpg", result)
|
assert.Equal(t, "testdata/chameleon_lime.jpg", result)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -59,77 +59,77 @@ func TestFileType_FindFirst(t *testing.T) {
|
||||||
dirs := []string{HiddenPath}
|
dirs := []string{HiddenPath}
|
||||||
|
|
||||||
t.Run("find xmp", func(t *testing.T) {
|
t.Run("find xmp", func(t *testing.T) {
|
||||||
result := TypeXMP.FindFirst("testdata/test.jpg", dirs, "", false)
|
result := FormatXMP.FindFirst("testdata/test.jpg", dirs, "", false)
|
||||||
assert.Equal(t, "testdata/.photoprism/test.xmp", result)
|
assert.Equal(t, "testdata/.photoprism/test.xmp", result)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("find xmp upper ext", func(t *testing.T) {
|
t.Run("find xmp upper ext", func(t *testing.T) {
|
||||||
result := TypeXMP.FindFirst("testdata/test.PNG", dirs, "", false)
|
result := FormatXMP.FindFirst("testdata/test.PNG", dirs, "", false)
|
||||||
assert.Equal(t, "testdata/.photoprism/test.xmp", result)
|
assert.Equal(t, "testdata/.photoprism/test.xmp", result)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("find xmp without sequence", func(t *testing.T) {
|
t.Run("find xmp without sequence", func(t *testing.T) {
|
||||||
result := TypeXMP.FindFirst("testdata/test (2).jpg", dirs, "", false)
|
result := FormatXMP.FindFirst("testdata/test (2).jpg", dirs, "", false)
|
||||||
assert.Equal(t, "", result)
|
assert.Equal(t, "", result)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("find xmp with sequence", func(t *testing.T) {
|
t.Run("find xmp with sequence", func(t *testing.T) {
|
||||||
result := TypeXMP.FindFirst("testdata/test (2).jpg", dirs, "", true)
|
result := FormatXMP.FindFirst("testdata/test (2).jpg", dirs, "", true)
|
||||||
assert.Equal(t, "testdata/.photoprism/test.xmp", result)
|
assert.Equal(t, "testdata/.photoprism/test.xmp", result)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("find jpg", func(t *testing.T) {
|
t.Run("find jpg", func(t *testing.T) {
|
||||||
result := TypeJpeg.FindFirst("testdata/test.xmp", dirs, "", false)
|
result := FormatJpeg.FindFirst("testdata/test.xmp", dirs, "", false)
|
||||||
assert.Equal(t, "testdata/test.jpg", result)
|
assert.Equal(t, "testdata/test.jpg", result)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("find jpg abs", func(t *testing.T) {
|
t.Run("find jpg abs", func(t *testing.T) {
|
||||||
result := TypeJpeg.FindFirst(Abs("testdata/test.xmp"), dirs, "", false)
|
result := FormatJpeg.FindFirst(Abs("testdata/test.xmp"), dirs, "", false)
|
||||||
assert.Equal(t, Abs("testdata/test.jpg"), result)
|
assert.Equal(t, Abs("testdata/test.jpg"), result)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("upper ext", func(t *testing.T) {
|
t.Run("upper ext", func(t *testing.T) {
|
||||||
result := TypeJpeg.FindFirst("testdata/test.XMP", dirs, "", false)
|
result := FormatJpeg.FindFirst("testdata/test.XMP", dirs, "", false)
|
||||||
assert.Equal(t, "testdata/test.jpg", result)
|
assert.Equal(t, "testdata/test.jpg", result)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("with sequence", func(t *testing.T) {
|
t.Run("with sequence", func(t *testing.T) {
|
||||||
result := TypeJpeg.FindFirst("testdata/test (2).xmp", dirs, "", false)
|
result := FormatJpeg.FindFirst("testdata/test (2).xmp", dirs, "", false)
|
||||||
assert.Equal(t, "", result)
|
assert.Equal(t, "", result)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("strip sequence", func(t *testing.T) {
|
t.Run("strip sequence", func(t *testing.T) {
|
||||||
result := TypeJpeg.FindFirst("testdata/test (2).xmp", dirs, "", true)
|
result := FormatJpeg.FindFirst("testdata/test (2).xmp", dirs, "", true)
|
||||||
assert.Equal(t, "testdata/test.jpg", result)
|
assert.Equal(t, "testdata/test.jpg", result)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("name upper", func(t *testing.T) {
|
t.Run("name upper", func(t *testing.T) {
|
||||||
result := TypeJpeg.FindFirst("testdata/CATYELLOW.xmp", dirs, "", true)
|
result := FormatJpeg.FindFirst("testdata/CATYELLOW.xmp", dirs, "", true)
|
||||||
assert.Equal(t, "testdata/CATYELLOW.jpg", result)
|
assert.Equal(t, "testdata/CATYELLOW.jpg", result)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("name lower", func(t *testing.T) {
|
t.Run("name lower", func(t *testing.T) {
|
||||||
result := TypeJpeg.FindFirst("testdata/chameleon_lime.xmp", dirs, "", true)
|
result := FormatJpeg.FindFirst("testdata/chameleon_lime.xmp", dirs, "", true)
|
||||||
assert.Equal(t, "testdata/chameleon_lime.jpg", result)
|
assert.Equal(t, "testdata/chameleon_lime.jpg", result)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("example_bmp_notfound", func(t *testing.T) {
|
t.Run("example_bmp_notfound", func(t *testing.T) {
|
||||||
result := TypeBitmap.FindFirst("testdata/example.00001.jpg", dirs, "", true)
|
result := FormatBitmap.FindFirst("testdata/example.00001.jpg", dirs, "", true)
|
||||||
assert.Equal(t, "", result)
|
assert.Equal(t, "", result)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("example_bmp_found", func(t *testing.T) {
|
t.Run("example_bmp_found", func(t *testing.T) {
|
||||||
result := TypeBitmap.FindFirst("testdata/example.00001.jpg", []string{"directory"}, "", true)
|
result := FormatBitmap.FindFirst("testdata/example.00001.jpg", []string{"directory"}, "", true)
|
||||||
assert.Equal(t, "testdata/directory/example.bmp", result)
|
assert.Equal(t, "testdata/directory/example.bmp", result)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("example_png_found", func(t *testing.T) {
|
t.Run("example_png_found", func(t *testing.T) {
|
||||||
result := TypePng.FindFirst("testdata/example.00001.jpg", []string{"directory", "directory/subdirectory"}, "", true)
|
result := FormatPng.FindFirst("testdata/example.00001.jpg", []string{"directory", "directory/subdirectory"}, "", true)
|
||||||
assert.Equal(t, "testdata/directory/subdirectory/example.png", result)
|
assert.Equal(t, "testdata/directory/subdirectory/example.png", result)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("example_bmp_found", func(t *testing.T) {
|
t.Run("example_bmp_found", func(t *testing.T) {
|
||||||
result := TypeBitmap.FindFirst(Abs("testdata/example.00001.jpg"), []string{"directory"}, Abs("testdata"), true)
|
result := FormatBitmap.FindFirst(Abs("testdata/example.00001.jpg"), []string{"directory"}, Abs("testdata"), true)
|
||||||
assert.Equal(t, Abs("testdata/directory/example.bmp"), result)
|
assert.Equal(t, Abs("testdata/directory/example.bmp"), result)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,35 +10,36 @@ const (
|
||||||
MediaOther MediaType = "other"
|
MediaOther MediaType = "other"
|
||||||
)
|
)
|
||||||
|
|
||||||
var MediaTypes = map[FileType]MediaType{
|
var MediaTypes = map[FileFormat]MediaType{
|
||||||
TypeRaw: MediaRaw,
|
FormatRaw: MediaRaw,
|
||||||
TypeJpeg: MediaImage,
|
FormatJpeg: MediaImage,
|
||||||
TypePng: MediaImage,
|
FormatPng: MediaImage,
|
||||||
TypeGif: MediaImage,
|
FormatGif: MediaImage,
|
||||||
TypeTiff: MediaImage,
|
FormatTiff: MediaImage,
|
||||||
TypeBitmap: MediaImage,
|
FormatBitmap: MediaImage,
|
||||||
TypeHEIF: MediaImage,
|
FormatHEIF: MediaImage,
|
||||||
TypeAvi: MediaVideo,
|
FormatAvi: MediaVideo,
|
||||||
TypeHEVC: MediaVideo,
|
FormatHEVC: MediaVideo,
|
||||||
TypeMp4: MediaVideo,
|
FormatAvc: MediaVideo,
|
||||||
TypeMov: MediaVideo,
|
FormatMp4: MediaVideo,
|
||||||
Type3gp: MediaVideo,
|
FormatMov: MediaVideo,
|
||||||
Type3g2: MediaVideo,
|
Format3gp: MediaVideo,
|
||||||
TypeFlv: MediaVideo,
|
Format3g2: MediaVideo,
|
||||||
TypeMkv: MediaVideo,
|
FormatFlv: MediaVideo,
|
||||||
TypeMpg: MediaVideo,
|
FormatMkv: MediaVideo,
|
||||||
TypeOgv: MediaVideo,
|
FormatMpg: MediaVideo,
|
||||||
TypeWebm: MediaVideo,
|
FormatOgv: MediaVideo,
|
||||||
TypeWMV: MediaVideo,
|
FormatWebm: MediaVideo,
|
||||||
TypeXMP: MediaSidecar,
|
FormatWMV: MediaVideo,
|
||||||
TypeXML: MediaSidecar,
|
FormatXMP: MediaSidecar,
|
||||||
TypeAAE: MediaSidecar,
|
FormatXML: MediaSidecar,
|
||||||
TypeYaml: MediaSidecar,
|
FormatAAE: MediaSidecar,
|
||||||
TypeText: MediaSidecar,
|
FormatYaml: MediaSidecar,
|
||||||
TypeJson: MediaSidecar,
|
FormatText: MediaSidecar,
|
||||||
TypeToml: MediaSidecar,
|
FormatJson: MediaSidecar,
|
||||||
TypeMarkdown: MediaSidecar,
|
FormatToml: MediaSidecar,
|
||||||
TypeOther: MediaOther,
|
FormatMarkdown: MediaSidecar,
|
||||||
|
FormatOther: MediaOther,
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetMediaType(fileName string) MediaType {
|
func GetMediaType(fileName string) MediaType {
|
||||||
|
@ -46,7 +47,7 @@ func GetMediaType(fileName string) MediaType {
|
||||||
return MediaOther
|
return MediaOther
|
||||||
}
|
}
|
||||||
|
|
||||||
result, ok := MediaTypes[GetFileType(fileName)]
|
result, ok := MediaTypes[GetFileFormat(fileName)]
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
result = MediaOther
|
result = MediaOther
|
||||||
|
|
|
@ -77,7 +77,7 @@ func TestSkipWalk(t *testing.T) {
|
||||||
|
|
||||||
done[fileName] = Found
|
done[fileName] = Found
|
||||||
|
|
||||||
if textName := TypeText.Find(fileName, false); textName != "" {
|
if textName := FormatText.Find(fileName, false); textName != "" {
|
||||||
done[textName] = Found
|
done[textName] = Found
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue