Videos: Enforce transcoding to MPEG-4 AVC #603

This commit is contained in:
Michael Mayer 2020-12-12 17:20:31 +01:00
parent f3d60ae095
commit 473566f404
30 changed files with 342 additions and 295 deletions

View file

@ -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)">

View file

@ -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;

View 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,
}, },
]; ];

View file

@ -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 {

View file

@ -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)

View file

@ -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);

View file

@ -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 {

View file

@ -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", () => {

View 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") != "" {

View file

@ -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.

View file

@ -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
} }

View file

@ -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())
}) })
} }

View file

@ -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()

View file

@ -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)

View file

@ -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)

View file

@ -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
} }

View file

@ -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 {

View file

@ -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 {

View file

@ -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())
} }

View file

@ -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 {

View file

@ -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")

View file

@ -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)

View file

@ -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)
}) })
} }

View file

@ -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,
} }

View file

@ -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
View 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 = ""
)

View file

@ -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)

View file

@ -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)
}) })
} }

View file

@ -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

View file

@ -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
} }