mirror of
https://github.com/sigmasternchen/tagify
synced 2025-03-15 07:08:55 +00:00
feat: added internal logic for managing directories, files and tags, reading, writing and creating of repositories
This commit is contained in:
parent
bbd189ef59
commit
7ed8d49f55
7 changed files with 319 additions and 0 deletions
5
src/database/config-keys.ts
Normal file
5
src/database/config-keys.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
const KEY_DIRECTORIES = "directories"
|
||||
const KEY_FILES = "files"
|
||||
|
||||
const KEY_FILE_PATH = "path"
|
||||
const KEY_FILE_TAGS = "tags"
|
73
src/database/database.ts
Normal file
73
src/database/database.ts
Normal file
|
@ -0,0 +1,73 @@
|
|||
class Database {
|
||||
private directories: Directory[]
|
||||
private files: MediaFile[]
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
public query(): Query {
|
||||
return new Query(this.files)
|
||||
}
|
||||
|
||||
public addDirectory(path: string) {
|
||||
this.directories.push(new Directory(path))
|
||||
}
|
||||
|
||||
public updateFile(file: MediaFile) {
|
||||
this.files = this.files.filter(f => f.getPath() != file.getPath())
|
||||
this.files.push(file)
|
||||
}
|
||||
|
||||
public findUntaggedPathsInDb(): string[] {
|
||||
return this.files
|
||||
.filter(f => f.getTags().length == 0)
|
||||
.map(f => f.getPath())
|
||||
}
|
||||
|
||||
public async forEachDirectory(callback: (d: string) => Promise<void>): Promise<void> {
|
||||
for (let i = 0; i < this.directories.length; i++) {
|
||||
await callback(this.directories[i].getPath())
|
||||
}
|
||||
}
|
||||
|
||||
public hasFile(path: string): boolean {
|
||||
return this.files.filter(f => f.getPath() == path).length != 0
|
||||
}
|
||||
|
||||
public getFile(path: string): MediaFile {
|
||||
const files = this.files.filter(f => f.getPath())
|
||||
|
||||
if (files.length == 1) {
|
||||
return files[0]
|
||||
} else {
|
||||
return new MediaFile(path)
|
||||
}
|
||||
}
|
||||
|
||||
public static fromRaw(obj: any): Database {
|
||||
const db = new Database()
|
||||
|
||||
if (!obj[KEY_DIRECTORIES] || typeof obj[KEY_DIRECTORIES] != "object") {
|
||||
throw "directories attribute not found"
|
||||
}
|
||||
db.directories = obj[KEY_DIRECTORIES].map((d: any) => Directory.fromRaw(d))
|
||||
|
||||
if (!obj[KEY_FILES] || typeof obj[KEY_FILES] != "object") {
|
||||
throw "files attribute not found"
|
||||
}
|
||||
db.files = obj[KEY_FILES].map((d: any) => MediaFile.fromRaw(d))
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
public toRaw(): any {
|
||||
let obj = {}
|
||||
|
||||
// @ts-ignore
|
||||
obj[KEY_DIRECTORIES] = this.directories.map(d => d.toRaw())
|
||||
// @ts-ignore
|
||||
obj[KEY_FILES] = this.files.map(f => f.toRaw())
|
||||
|
||||
return obj
|
||||
}
|
||||
}
|
23
src/database/directory.ts
Normal file
23
src/database/directory.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
class Directory {
|
||||
private readonly path: string
|
||||
|
||||
constructor(path: string) {
|
||||
this.path = path
|
||||
}
|
||||
|
||||
public getPath(): string {
|
||||
return this.path
|
||||
}
|
||||
|
||||
static fromRaw(obj: any): Directory {
|
||||
if (typeof obj != "string") {
|
||||
throw "directory has to be string"
|
||||
}
|
||||
|
||||
return new Directory(obj)
|
||||
}
|
||||
|
||||
public toRaw(): any {
|
||||
return this.path
|
||||
}
|
||||
}
|
31
src/database/file-type.ts
Normal file
31
src/database/file-type.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
enum MediaFileType {
|
||||
unknown,
|
||||
video,
|
||||
image
|
||||
}
|
||||
|
||||
function FileTypeFromExtension(name: string): MediaFileType {
|
||||
while (name.indexOf(".") >= 0) {
|
||||
name = name.substring(name.indexOf(".") + 1)
|
||||
}
|
||||
name = name.toLowerCase()
|
||||
switch (name) {
|
||||
case "jpeg":
|
||||
case "jpg":
|
||||
case "gif":
|
||||
case "png":
|
||||
case "webp":
|
||||
return MediaFileType.image
|
||||
case "mp4":
|
||||
case "avi":
|
||||
case "mpeg":
|
||||
case "mpeg2":
|
||||
case "m4v":
|
||||
case "mkv":
|
||||
case "mov":
|
||||
case "webv":
|
||||
return MediaFileType.video
|
||||
default:
|
||||
return MediaFileType.unknown
|
||||
}
|
||||
}
|
71
src/database/file.ts
Normal file
71
src/database/file.ts
Normal file
|
@ -0,0 +1,71 @@
|
|||
class MediaFile {
|
||||
private readonly path: string
|
||||
private readonly type: MediaFileType
|
||||
private tags: string[]
|
||||
|
||||
constructor(path: string) {
|
||||
this.path = path
|
||||
this.type = FileTypeFromExtension(path)
|
||||
}
|
||||
|
||||
public getPath(): string {
|
||||
return this.path
|
||||
}
|
||||
|
||||
public getType(): MediaFileType {
|
||||
return this.type
|
||||
}
|
||||
|
||||
public getTags(): string[] {
|
||||
return this.tags
|
||||
}
|
||||
|
||||
public addTag(tag: string) {
|
||||
if (this.tags.indexOf(tag) < 0) {
|
||||
this.tags.push(tag)
|
||||
}
|
||||
}
|
||||
|
||||
public removeTag(tag: string) {
|
||||
const i = this.tags.indexOf(tag)
|
||||
if (i >= 0) {
|
||||
this.tags.slice(i, 1)
|
||||
}
|
||||
}
|
||||
|
||||
public hasTag(tag: string): boolean {
|
||||
return this.tags.indexOf(tag) >= 0
|
||||
}
|
||||
|
||||
|
||||
static fromRaw(obj: any): MediaFile {
|
||||
if (typeof obj[KEY_FILE_PATH] != "string") {
|
||||
throw "path attribute has to be string"
|
||||
}
|
||||
|
||||
const file = new MediaFile(obj[KEY_FILE_PATH])
|
||||
|
||||
if (!obj[KEY_FILE_TAGS]) {
|
||||
throw "tags attribute missing"
|
||||
}
|
||||
file.tags = obj[KEY_FILE_TAGS].map((t: any) => {
|
||||
if (typeof t != "string") {
|
||||
throw "tags have to be strings"
|
||||
}
|
||||
return t
|
||||
})
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
public toRaw(): any {
|
||||
const obj = {}
|
||||
|
||||
// @ts-ignore
|
||||
obj[KEY_FILE_PATH] = this.path
|
||||
// @ts-ignore
|
||||
obj[KEY_FILE_TAGS] = this.tags
|
||||
|
||||
return obj
|
||||
}
|
||||
}
|
56
src/database/query.ts
Normal file
56
src/database/query.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
type CountedTag = {
|
||||
name: string,
|
||||
count: number
|
||||
}
|
||||
|
||||
|
||||
class Query {
|
||||
private readonly files: MediaFile[]
|
||||
|
||||
public constructor(files: MediaFile[]) {
|
||||
this.files = [...files]
|
||||
}
|
||||
|
||||
public tags(): string[] {
|
||||
return this.files
|
||||
.map(f => f.getTags())
|
||||
.flat()
|
||||
.filter((v, i, s) => s.indexOf(v) == i)
|
||||
}
|
||||
|
||||
public countedTags(): CountedTag[] {
|
||||
return this.tags()
|
||||
.map(t => ({
|
||||
name: t,
|
||||
count: this.files
|
||||
.filter(f => f.hasTag(t))
|
||||
.length
|
||||
}))
|
||||
}
|
||||
|
||||
public count(): number {
|
||||
return this.files.length
|
||||
}
|
||||
|
||||
public has(tag: string): Query {
|
||||
return new Query(
|
||||
this.files.filter(f => f.hasTag(tag))
|
||||
)
|
||||
}
|
||||
|
||||
public hasNot(tag: string): Query {
|
||||
return new Query(
|
||||
this.files.filter(f => !f.hasTag(tag))
|
||||
)
|
||||
}
|
||||
|
||||
public is(type: MediaFileType): Query {
|
||||
return new Query(
|
||||
this.files.filter(f => f.getType() == type)
|
||||
)
|
||||
}
|
||||
|
||||
public get(): MediaFile[] {
|
||||
return this.files
|
||||
}
|
||||
}
|
60
src/repository/index.ts
Normal file
60
src/repository/index.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
import * as fsPromises from "fs/promises";
|
||||
|
||||
const DB_FILE = ".tagdb.json"
|
||||
|
||||
class Repository {
|
||||
private readonly path: string
|
||||
private cached?: Database
|
||||
|
||||
constructor(path: string) {
|
||||
this.path = path
|
||||
}
|
||||
|
||||
public async get(): Promise<Database> {
|
||||
if (!this.cached) {
|
||||
const buffer = await fsPromises.readFile(this.path + "/" + DB_FILE)
|
||||
const raw = JSON.parse(buffer.toString())
|
||||
this.cached = Database.fromRaw(raw)
|
||||
}
|
||||
return this.cached
|
||||
}
|
||||
|
||||
public async reload(): Promise<Database> {
|
||||
this.cached = undefined
|
||||
return this.get()
|
||||
}
|
||||
|
||||
public async exists(): Promise<boolean> {
|
||||
try {
|
||||
await this.get()
|
||||
return true
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public new(db: Database) {
|
||||
this.cached = db
|
||||
}
|
||||
|
||||
public async write(): Promise<void> {
|
||||
await fsPromises.writeFile(this.path + "/" + DB_FILE, JSON.stringify(this.cached.toRaw()))
|
||||
}
|
||||
|
||||
public async findUntagged(): Promise<string[]> {
|
||||
const db = await this.get()
|
||||
const untagged = db.findUntaggedPathsInDb()
|
||||
|
||||
await db.forEachDirectory(async d => {
|
||||
const entries = await fsPromises.readdir(this.path + "/" + d, {withFileTypes: true})
|
||||
entries
|
||||
.filter(e => e.isFile())
|
||||
.map(e => e.name)
|
||||
.filter(f => FileTypeFromExtension(f) != MediaFileType.unknown)
|
||||
.filter(f => !db.hasFile(f))
|
||||
.forEach(f => untagged.push(f))
|
||||
})
|
||||
|
||||
return untagged.filter((v, i, s) => s.indexOf(v) == i)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue