serato-tools 1.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- serato_tools-1.5.0/LICENSE +19 -0
- serato_tools-1.5.0/PKG-INFO +60 -0
- serato_tools-1.5.0/README.md +28 -0
- serato_tools-1.5.0/pyproject.toml +25 -0
- serato_tools-1.5.0/serato_tools.egg-info/PKG-INFO +60 -0
- serato_tools-1.5.0/serato_tools.egg-info/SOURCES.txt +20 -0
- serato_tools-1.5.0/serato_tools.egg-info/dependency_links.txt +1 -0
- serato_tools-1.5.0/serato_tools.egg-info/requires.txt +4 -0
- serato_tools-1.5.0/serato_tools.egg-info/top_level.txt +1 -0
- serato_tools-1.5.0/setup.cfg +4 -0
- serato_tools-1.5.0/src/__init__.py +0 -0
- serato_tools-1.5.0/src/database_v2.py +212 -0
- serato_tools-1.5.0/src/serato_autotags.py +101 -0
- serato_tools-1.5.0/src/serato_beatgrid.py +69 -0
- serato_tools-1.5.0/src/serato_gain.py +21 -0
- serato_tools-1.5.0/src/serato_markers2.py +530 -0
- serato_tools-1.5.0/src/serato_markers_.py +359 -0
- serato_tools-1.5.0/src/serato_overview.py +67 -0
- serato_tools-1.5.0/src/tagdump.py +78 -0
- serato_tools-1.5.0/src/utils/__init__.py +0 -0
- serato_tools-1.5.0/src/utils/tags.py +30 -0
- serato_tools-1.5.0/src/utils/ui.py +39 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright 2019 Jan Holthuis
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
4
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
5
|
+
the Software without restriction, including without limitation the rights to
|
|
6
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
7
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
|
8
|
+
so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: serato_tools
|
|
3
|
+
Version: 1.5.0
|
|
4
|
+
Summary: Serato track metadata tags
|
|
5
|
+
Author-email: bvandercar-vt <bvandercar@outlook.com>
|
|
6
|
+
License: Copyright 2019 Jan Holthuis
|
|
7
|
+
|
|
8
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
9
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
10
|
+
the Software without restriction, including without limitation the rights to
|
|
11
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
12
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
|
13
|
+
so, subject to the following conditions:
|
|
14
|
+
|
|
15
|
+
The above copyright notice and this permission notice shall be included in all
|
|
16
|
+
copies or substantial portions of the Software.
|
|
17
|
+
|
|
18
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
19
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
20
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
21
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
22
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
23
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
24
|
+
SOFTWARE.
|
|
25
|
+
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: mutagen
|
|
29
|
+
Provides-Extra: waveform-drawing
|
|
30
|
+
Requires-Dist: pillow; extra == "waveform-drawing"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
Fork of https://github.com/Holzhaus/serato-tags , which appears to be no longer maintained.
|
|
34
|
+
|
|
35
|
+
# Installation
|
|
36
|
+
|
|
37
|
+
Can install using pip:
|
|
38
|
+
|
|
39
|
+
`pip install git+https://github.com/bvandercar-vt/serato-tags@main`
|
|
40
|
+
|
|
41
|
+
Or add this line to your `requirements.txt`:
|
|
42
|
+
|
|
43
|
+
`git+https://github.com/bvandercar-vt/serato-tags@main`
|
|
44
|
+
|
|
45
|
+
# Serato Tags
|
|
46
|
+
|
|
47
|
+
This repository aims to document the GEOB ID3 tags that the Serato DJ software uses to store its metadata.
|
|
48
|
+
You can also have a look at [this lengthy blog post](https://homepage.ruhr-uni-bochum.de/jan.holthuis/posts/reversing-seratos-geob-tags) that goes into detail how I reversed the contents of the `Serato Markers2` GEOB tag.
|
|
49
|
+
|
|
50
|
+
| Tag | Progress | Contents | Example script
|
|
51
|
+
| -------------------------------------------- | ------------- | -------------------------- | --------------
|
|
52
|
+
| [`Serato Analysis`](docs/serato_analysis.md) | Done | Serato version information |
|
|
53
|
+
| [`Serato Autotags`](docs/serato_autotags.md) | Done | BPM and Gain values | [`serato_autotags.py`](scripts/serato_autotags.py)
|
|
54
|
+
| [`Serato BeatGrid`](docs/serato_beatgrid.md) | Mostly done | Beatgrid Markers | [`serato_beatgrid.py`](scripts/serato_beatgrid.py)
|
|
55
|
+
| [`Serato Markers2`](docs/serato_markers2.md) | Mostly done | Hotcues, Saved Loops, etc. | [`serato_markers2.py`](scripts/serato_markers2.py)
|
|
56
|
+
| [`Serato Markers_`](docs/serato_markers_.md) | Mostly done | Hotcues, Saved Loops, etc. | [`serato_markers_.py`](scripts/serato_markers_.py)
|
|
57
|
+
| [`Serato Offsets_`](docs/serato_offsets_.md) | *Not started* | |
|
|
58
|
+
| [`Serato Overview`](docs/serato_overview.md) | Done | Waveform data | [`serato_overview.py`](scripts/serato_overview.py)
|
|
59
|
+
|
|
60
|
+
The different file/tag formats that Serato uses to store the information are documented in [`docs/fileformats.md`](docs/fileformats.md), a script to dump the tag data can be found at [`scripts/tagdump.py`](scripts/tagdump.py).
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Fork of https://github.com/Holzhaus/serato-tags , which appears to be no longer maintained.
|
|
2
|
+
|
|
3
|
+
# Installation
|
|
4
|
+
|
|
5
|
+
Can install using pip:
|
|
6
|
+
|
|
7
|
+
`pip install git+https://github.com/bvandercar-vt/serato-tags@main`
|
|
8
|
+
|
|
9
|
+
Or add this line to your `requirements.txt`:
|
|
10
|
+
|
|
11
|
+
`git+https://github.com/bvandercar-vt/serato-tags@main`
|
|
12
|
+
|
|
13
|
+
# Serato Tags
|
|
14
|
+
|
|
15
|
+
This repository aims to document the GEOB ID3 tags that the Serato DJ software uses to store its metadata.
|
|
16
|
+
You can also have a look at [this lengthy blog post](https://homepage.ruhr-uni-bochum.de/jan.holthuis/posts/reversing-seratos-geob-tags) that goes into detail how I reversed the contents of the `Serato Markers2` GEOB tag.
|
|
17
|
+
|
|
18
|
+
| Tag | Progress | Contents | Example script
|
|
19
|
+
| -------------------------------------------- | ------------- | -------------------------- | --------------
|
|
20
|
+
| [`Serato Analysis`](docs/serato_analysis.md) | Done | Serato version information |
|
|
21
|
+
| [`Serato Autotags`](docs/serato_autotags.md) | Done | BPM and Gain values | [`serato_autotags.py`](scripts/serato_autotags.py)
|
|
22
|
+
| [`Serato BeatGrid`](docs/serato_beatgrid.md) | Mostly done | Beatgrid Markers | [`serato_beatgrid.py`](scripts/serato_beatgrid.py)
|
|
23
|
+
| [`Serato Markers2`](docs/serato_markers2.md) | Mostly done | Hotcues, Saved Loops, etc. | [`serato_markers2.py`](scripts/serato_markers2.py)
|
|
24
|
+
| [`Serato Markers_`](docs/serato_markers_.md) | Mostly done | Hotcues, Saved Loops, etc. | [`serato_markers_.py`](scripts/serato_markers_.py)
|
|
25
|
+
| [`Serato Offsets_`](docs/serato_offsets_.md) | *Not started* | |
|
|
26
|
+
| [`Serato Overview`](docs/serato_overview.md) | Done | Waveform data | [`serato_overview.py`](scripts/serato_overview.py)
|
|
27
|
+
|
|
28
|
+
The different file/tag formats that Serato uses to store the information are documented in [`docs/fileformats.md`](docs/fileformats.md), a script to dump the tag data can be found at [`scripts/tagdump.py`](scripts/tagdump.py).
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "serato_tools"
|
|
7
|
+
version = "1.5.0"
|
|
8
|
+
description = "Serato track metadata tags"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "bvandercar-vt", email = "bvandercar@outlook.com" }
|
|
12
|
+
]
|
|
13
|
+
license = { file = "LICENSE" }
|
|
14
|
+
dependencies = [
|
|
15
|
+
"mutagen"
|
|
16
|
+
]
|
|
17
|
+
optional-dependencies.waveform_drawing = [
|
|
18
|
+
"pillow"
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[tool.setuptools]
|
|
22
|
+
packages = ["serato_tools", "serato_tools.utils"]
|
|
23
|
+
|
|
24
|
+
[tool.setuptools.package-dir]
|
|
25
|
+
serato_tools = "src"
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: serato_tools
|
|
3
|
+
Version: 1.5.0
|
|
4
|
+
Summary: Serato track metadata tags
|
|
5
|
+
Author-email: bvandercar-vt <bvandercar@outlook.com>
|
|
6
|
+
License: Copyright 2019 Jan Holthuis
|
|
7
|
+
|
|
8
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
9
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
10
|
+
the Software without restriction, including without limitation the rights to
|
|
11
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
12
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
|
13
|
+
so, subject to the following conditions:
|
|
14
|
+
|
|
15
|
+
The above copyright notice and this permission notice shall be included in all
|
|
16
|
+
copies or substantial portions of the Software.
|
|
17
|
+
|
|
18
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
19
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
20
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
21
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
22
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
23
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
24
|
+
SOFTWARE.
|
|
25
|
+
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: mutagen
|
|
29
|
+
Provides-Extra: waveform-drawing
|
|
30
|
+
Requires-Dist: pillow; extra == "waveform-drawing"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
Fork of https://github.com/Holzhaus/serato-tags , which appears to be no longer maintained.
|
|
34
|
+
|
|
35
|
+
# Installation
|
|
36
|
+
|
|
37
|
+
Can install using pip:
|
|
38
|
+
|
|
39
|
+
`pip install git+https://github.com/bvandercar-vt/serato-tags@main`
|
|
40
|
+
|
|
41
|
+
Or add this line to your `requirements.txt`:
|
|
42
|
+
|
|
43
|
+
`git+https://github.com/bvandercar-vt/serato-tags@main`
|
|
44
|
+
|
|
45
|
+
# Serato Tags
|
|
46
|
+
|
|
47
|
+
This repository aims to document the GEOB ID3 tags that the Serato DJ software uses to store its metadata.
|
|
48
|
+
You can also have a look at [this lengthy blog post](https://homepage.ruhr-uni-bochum.de/jan.holthuis/posts/reversing-seratos-geob-tags) that goes into detail how I reversed the contents of the `Serato Markers2` GEOB tag.
|
|
49
|
+
|
|
50
|
+
| Tag | Progress | Contents | Example script
|
|
51
|
+
| -------------------------------------------- | ------------- | -------------------------- | --------------
|
|
52
|
+
| [`Serato Analysis`](docs/serato_analysis.md) | Done | Serato version information |
|
|
53
|
+
| [`Serato Autotags`](docs/serato_autotags.md) | Done | BPM and Gain values | [`serato_autotags.py`](scripts/serato_autotags.py)
|
|
54
|
+
| [`Serato BeatGrid`](docs/serato_beatgrid.md) | Mostly done | Beatgrid Markers | [`serato_beatgrid.py`](scripts/serato_beatgrid.py)
|
|
55
|
+
| [`Serato Markers2`](docs/serato_markers2.md) | Mostly done | Hotcues, Saved Loops, etc. | [`serato_markers2.py`](scripts/serato_markers2.py)
|
|
56
|
+
| [`Serato Markers_`](docs/serato_markers_.md) | Mostly done | Hotcues, Saved Loops, etc. | [`serato_markers_.py`](scripts/serato_markers_.py)
|
|
57
|
+
| [`Serato Offsets_`](docs/serato_offsets_.md) | *Not started* | |
|
|
58
|
+
| [`Serato Overview`](docs/serato_overview.md) | Done | Waveform data | [`serato_overview.py`](scripts/serato_overview.py)
|
|
59
|
+
|
|
60
|
+
The different file/tag formats that Serato uses to store the information are documented in [`docs/fileformats.md`](docs/fileformats.md), a script to dump the tag data can be found at [`scripts/tagdump.py`](scripts/tagdump.py).
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
serato_tools.egg-info/PKG-INFO
|
|
5
|
+
serato_tools.egg-info/SOURCES.txt
|
|
6
|
+
serato_tools.egg-info/dependency_links.txt
|
|
7
|
+
serato_tools.egg-info/requires.txt
|
|
8
|
+
serato_tools.egg-info/top_level.txt
|
|
9
|
+
src/__init__.py
|
|
10
|
+
src/database_v2.py
|
|
11
|
+
src/serato_autotags.py
|
|
12
|
+
src/serato_beatgrid.py
|
|
13
|
+
src/serato_gain.py
|
|
14
|
+
src/serato_markers2.py
|
|
15
|
+
src/serato_markers_.py
|
|
16
|
+
src/serato_overview.py
|
|
17
|
+
src/tagdump.py
|
|
18
|
+
src/utils/__init__.py
|
|
19
|
+
src/utils/tags.py
|
|
20
|
+
src/utils/ui.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
serato_tools
|
|
File without changes
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
import io
|
|
4
|
+
import os
|
|
5
|
+
import struct
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Any, Callable, Generator, Iterable, Tuple, TypedDict
|
|
8
|
+
|
|
9
|
+
DATABASE_FILE = os.path.join(os.path.expanduser("~"), "Music\\_Serato_\\database V2")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DbEntry(TypedDict):
|
|
13
|
+
field: str
|
|
14
|
+
field_name: str
|
|
15
|
+
value: str | int | bool | list["DbEntry"]
|
|
16
|
+
size_bytes: int
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
FIELDNAMES = {
|
|
20
|
+
# Database
|
|
21
|
+
"vrsn": "Version",
|
|
22
|
+
"otrk": "Track",
|
|
23
|
+
"ttyp": "File Type",
|
|
24
|
+
"pfil": "File Path",
|
|
25
|
+
"tsng": "Song Title",
|
|
26
|
+
"tart": "Artist",
|
|
27
|
+
"talb": "Album",
|
|
28
|
+
"tgen": "Genre",
|
|
29
|
+
"tlen": "Length",
|
|
30
|
+
"tbit": "Bitrate",
|
|
31
|
+
"tsmp": "Sample Rate",
|
|
32
|
+
"tsiz": "Size",
|
|
33
|
+
"tbpm": "BPM",
|
|
34
|
+
"tkey": "Key",
|
|
35
|
+
"tart": "Artist",
|
|
36
|
+
"utme": "File Time",
|
|
37
|
+
"bmis": "Missing",
|
|
38
|
+
"tgrp": "Grouping",
|
|
39
|
+
"tlbl": "Publisher",
|
|
40
|
+
"ttyr": "Year",
|
|
41
|
+
# Serato stuff
|
|
42
|
+
"tadd": "Date added",
|
|
43
|
+
"uadd": "Date added",
|
|
44
|
+
"bbgl": "Beatgrid Locked",
|
|
45
|
+
# Crates
|
|
46
|
+
"osrt": "Sorting",
|
|
47
|
+
"brev": "Reverse Order",
|
|
48
|
+
"ovct": "Column Title",
|
|
49
|
+
"tvcn": "Column Name",
|
|
50
|
+
"tvcw": "Column Width",
|
|
51
|
+
"ptrk": "Track Path",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _get_type_id(name: str) -> str:
|
|
56
|
+
# vrsn field has no type_id, but contains text
|
|
57
|
+
return "t" if name == "vrsn" else name[0]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
ParsedType = Tuple[str, int, Any, bytes]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def parse(fp: io.BytesIO | io.BufferedReader) -> Generator[ParsedType]:
|
|
64
|
+
for header in iter(lambda: fp.read(8), b""):
|
|
65
|
+
assert len(header) == 8
|
|
66
|
+
name_ascii: bytes
|
|
67
|
+
length: int
|
|
68
|
+
name_ascii, length = struct.unpack(">4sI", header)
|
|
69
|
+
name: str = name_ascii.decode("ascii")
|
|
70
|
+
type_id: str = _get_type_id(name)
|
|
71
|
+
|
|
72
|
+
data = fp.read(length)
|
|
73
|
+
assert len(data) == length
|
|
74
|
+
|
|
75
|
+
value: Any
|
|
76
|
+
if type_id == "b":
|
|
77
|
+
value = struct.unpack("?", data)[0]
|
|
78
|
+
elif type_id in ("o", "r"):
|
|
79
|
+
value = tuple(parse(io.BytesIO(data)))
|
|
80
|
+
elif type_id in ("p", "t"):
|
|
81
|
+
value = (data[1:] + b"\00").decode("utf-16")
|
|
82
|
+
elif type_id == "s":
|
|
83
|
+
value = struct.unpack(">H", data)[0]
|
|
84
|
+
elif type_id == "u":
|
|
85
|
+
value = struct.unpack(">I", data)[0]
|
|
86
|
+
else:
|
|
87
|
+
value = data
|
|
88
|
+
|
|
89
|
+
yield name, length, value, data
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class ModifyRule(TypedDict):
|
|
93
|
+
field: str
|
|
94
|
+
func: Callable[[str, Any], Any]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def modify(
|
|
98
|
+
fp: io.BytesIO | io.BufferedWriter,
|
|
99
|
+
parsed: Iterable[ParsedType],
|
|
100
|
+
rules: list[ModifyRule] = [],
|
|
101
|
+
):
|
|
102
|
+
track_filename: str = ""
|
|
103
|
+
for name, length, value, data in parsed:
|
|
104
|
+
name_bytes = name.encode("ascii")
|
|
105
|
+
assert len(name_bytes) == 4
|
|
106
|
+
|
|
107
|
+
type_id: str = _get_type_id(name)
|
|
108
|
+
|
|
109
|
+
if name == "pfil":
|
|
110
|
+
assert isinstance(value, str)
|
|
111
|
+
track_filename = os.path.normpath(value)
|
|
112
|
+
|
|
113
|
+
rule_has_been_done = False
|
|
114
|
+
for rule in rules:
|
|
115
|
+
if name == rule["field"]:
|
|
116
|
+
maybe_new_value = rule["func"](track_filename, value)
|
|
117
|
+
if maybe_new_value is not None:
|
|
118
|
+
assert (
|
|
119
|
+
not rule_has_been_done
|
|
120
|
+
), f"Should only pass one rule per field (field: {name})"
|
|
121
|
+
rule_has_been_done = True
|
|
122
|
+
value = maybe_new_value
|
|
123
|
+
|
|
124
|
+
if type_id == "b":
|
|
125
|
+
data = struct.pack("?", value)
|
|
126
|
+
elif type_id in ("o", "r"):
|
|
127
|
+
assert isinstance(value, tuple)
|
|
128
|
+
nested_buffer = io.BytesIO()
|
|
129
|
+
modify(nested_buffer, value, rules)
|
|
130
|
+
data = nested_buffer.getvalue()
|
|
131
|
+
elif type_id in ("p", "t"):
|
|
132
|
+
new_data = str(value).encode("utf-16")[2:]
|
|
133
|
+
assert new_data[-1:] == b"\x00"
|
|
134
|
+
data = data[:1] + new_data[:-1]
|
|
135
|
+
elif type_id == "s":
|
|
136
|
+
data = struct.pack(">H", value)
|
|
137
|
+
elif type_id == "u":
|
|
138
|
+
data = struct.pack(">I", value)
|
|
139
|
+
|
|
140
|
+
length = len(data)
|
|
141
|
+
|
|
142
|
+
header = struct.pack(">4sI", name_bytes, length)
|
|
143
|
+
fp.write(header)
|
|
144
|
+
fp.write(data)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def modify_file(file: str, rules: list[ModifyRule]):
|
|
148
|
+
with open(file, "rb") as read_file:
|
|
149
|
+
parsed = list(parse(read_file))
|
|
150
|
+
|
|
151
|
+
output = io.BytesIO()
|
|
152
|
+
modify(output, parsed, rules)
|
|
153
|
+
|
|
154
|
+
with open(file, "wb") as write_file:
|
|
155
|
+
output.seek(0)
|
|
156
|
+
write_file.write(output.read())
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def parse_to_objects(fp: io.BytesIO | io.BufferedReader | str) -> Generator[DbEntry]:
|
|
160
|
+
if isinstance(fp, str):
|
|
161
|
+
fp = open(fp, "rb")
|
|
162
|
+
|
|
163
|
+
for name, length, value, data in parse(fp):
|
|
164
|
+
if isinstance(value, tuple):
|
|
165
|
+
try:
|
|
166
|
+
new_val: list[DbEntry] = [
|
|
167
|
+
{
|
|
168
|
+
"field": n,
|
|
169
|
+
"field_name": FIELDNAMES.get(n, "Unknown"),
|
|
170
|
+
"size_bytes": l,
|
|
171
|
+
"value": v,
|
|
172
|
+
}
|
|
173
|
+
for n, l, v, d in value
|
|
174
|
+
]
|
|
175
|
+
except:
|
|
176
|
+
print(f"error on {value}")
|
|
177
|
+
raise
|
|
178
|
+
value = new_val
|
|
179
|
+
else:
|
|
180
|
+
value = repr(value)
|
|
181
|
+
|
|
182
|
+
yield {
|
|
183
|
+
"field": name,
|
|
184
|
+
"field_name": FIELDNAMES.get(name, "Unknown"),
|
|
185
|
+
"size_bytes": length,
|
|
186
|
+
"value": value,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
if __name__ == "__main__":
|
|
191
|
+
import argparse
|
|
192
|
+
|
|
193
|
+
parser = argparse.ArgumentParser()
|
|
194
|
+
parser.add_argument(
|
|
195
|
+
"file",
|
|
196
|
+
type=argparse.FileType("rb"),
|
|
197
|
+
default=open(DATABASE_FILE, "rb"),
|
|
198
|
+
nargs="?",
|
|
199
|
+
)
|
|
200
|
+
args = parser.parse_args()
|
|
201
|
+
|
|
202
|
+
for entry in parse_to_objects(args.file):
|
|
203
|
+
if isinstance(entry["value"], list):
|
|
204
|
+
print(f"{entry['field']} ({entry['field_name']}, {entry['size_bytes']} B)")
|
|
205
|
+
for e in entry["value"]:
|
|
206
|
+
print(
|
|
207
|
+
f" {e['field']} ({e['field_name']}, {e['size_bytes']} B): {e['value']}"
|
|
208
|
+
)
|
|
209
|
+
else:
|
|
210
|
+
print(
|
|
211
|
+
f"{entry['field']} ({entry['field_name']}, {entry['size_bytes']} B): {entry['value']}"
|
|
212
|
+
)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
import io
|
|
4
|
+
import struct
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
FMT_VERSION = "BB"
|
|
8
|
+
|
|
9
|
+
GEOB_KEY = "Serato Autotags"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def readbytes(fp: io.BytesIO | io.BufferedReader):
|
|
13
|
+
for x in iter(lambda: fp.read(1), b""):
|
|
14
|
+
if x == b"\00":
|
|
15
|
+
break
|
|
16
|
+
yield x
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def parse(fp: io.BytesIO | io.BufferedReader):
|
|
20
|
+
version = struct.unpack(FMT_VERSION, fp.read(2))
|
|
21
|
+
assert version == (0x01, 0x01)
|
|
22
|
+
|
|
23
|
+
for i in range(3):
|
|
24
|
+
data = b"".join(readbytes(fp))
|
|
25
|
+
yield float(data.decode("ascii"))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def dump(bpm: float, autogain: float, gaindb: float):
|
|
29
|
+
data = struct.pack(FMT_VERSION, 0x01, 0x01)
|
|
30
|
+
for value, decimals in ((bpm, 2), (autogain, 3), (gaindb, 3)):
|
|
31
|
+
data += "{:.{}f}".format(value, decimals).encode("ascii")
|
|
32
|
+
data += b"\x00"
|
|
33
|
+
return data
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if __name__ == "__main__":
|
|
37
|
+
import argparse
|
|
38
|
+
import configparser
|
|
39
|
+
import subprocess
|
|
40
|
+
import tempfile
|
|
41
|
+
|
|
42
|
+
import mutagen._file
|
|
43
|
+
|
|
44
|
+
from .utils.tags import get_geob, tag_geob
|
|
45
|
+
from .utils.ui import get_text_editor
|
|
46
|
+
|
|
47
|
+
parser = argparse.ArgumentParser()
|
|
48
|
+
parser.add_argument("file")
|
|
49
|
+
parser.add_argument("-e", "--edit", action="store_true")
|
|
50
|
+
args = parser.parse_args()
|
|
51
|
+
|
|
52
|
+
tagfile = mutagen._file.File(args.file)
|
|
53
|
+
if tagfile is not None:
|
|
54
|
+
fp = io.BytesIO(get_geob(tagfile, GEOB_KEY))
|
|
55
|
+
else:
|
|
56
|
+
fp = open(args.file, mode="rb")
|
|
57
|
+
|
|
58
|
+
with fp:
|
|
59
|
+
bpm, autogain, gaindb = parse(fp)
|
|
60
|
+
|
|
61
|
+
if args.edit:
|
|
62
|
+
editor = get_text_editor()
|
|
63
|
+
|
|
64
|
+
with tempfile.NamedTemporaryFile() as f:
|
|
65
|
+
f.write(
|
|
66
|
+
(f"bpm: {bpm}\n" "autogain: {autogain}\n" "gaindb: {gaindb}\n").encode(
|
|
67
|
+
"ascii"
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
f.flush()
|
|
71
|
+
status = subprocess.call((editor, f.name))
|
|
72
|
+
f.seek(0)
|
|
73
|
+
output = f.read()
|
|
74
|
+
|
|
75
|
+
if status != 0:
|
|
76
|
+
error_str = (f"Command executation failed with status: {status}",)
|
|
77
|
+
print(error_str, file=sys.stderr)
|
|
78
|
+
raise Exception(error_str)
|
|
79
|
+
|
|
80
|
+
cp = configparser.ConfigParser()
|
|
81
|
+
try:
|
|
82
|
+
cp.read_string("[Autotags]\n" + output.decode())
|
|
83
|
+
bpm = cp.getfloat("Autotags", "bpm")
|
|
84
|
+
autogain = cp.getfloat("Autotags", "autogain")
|
|
85
|
+
gaindb = cp.getfloat("Autotags", "gaindb")
|
|
86
|
+
except Exception:
|
|
87
|
+
print("Invalid input, no changes made", file=sys.stderr)
|
|
88
|
+
raise
|
|
89
|
+
|
|
90
|
+
new_data = dump(bpm, autogain, gaindb)
|
|
91
|
+
if tagfile:
|
|
92
|
+
if tagfile is not None:
|
|
93
|
+
tag_geob(tagfile, GEOB_KEY, new_data)
|
|
94
|
+
tagfile.save()
|
|
95
|
+
else:
|
|
96
|
+
with open(args.file, mode="wb") as fp:
|
|
97
|
+
fp.write(new_data)
|
|
98
|
+
else:
|
|
99
|
+
print(f"BPM: {bpm}")
|
|
100
|
+
print(f"Auto Gain: {autogain}")
|
|
101
|
+
print(f"Gain dB: {gaindb}")
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
import collections
|
|
4
|
+
import io
|
|
5
|
+
import struct
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
FMT_VERSION = "BB"
|
|
9
|
+
|
|
10
|
+
GEOB_KEY = "Serato BeatGrid"
|
|
11
|
+
|
|
12
|
+
NonTerminalBeatgridMarker = collections.namedtuple(
|
|
13
|
+
"NonTerminalBeatgridMarker",
|
|
14
|
+
(
|
|
15
|
+
"position",
|
|
16
|
+
"beats_till_next_marker",
|
|
17
|
+
),
|
|
18
|
+
)
|
|
19
|
+
TerminalBeatgridMarker = collections.namedtuple(
|
|
20
|
+
"TerminalBeatgridMarker",
|
|
21
|
+
(
|
|
22
|
+
"position",
|
|
23
|
+
"bpm",
|
|
24
|
+
),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
Footer = collections.namedtuple("Footer", ("unknown",))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def parse(fp: io.BytesIO | io.BufferedReader):
|
|
31
|
+
version = struct.unpack(FMT_VERSION, fp.read(2))
|
|
32
|
+
assert version == (0x01, 0x00)
|
|
33
|
+
|
|
34
|
+
num_markers = struct.unpack(">I", fp.read(4))[0]
|
|
35
|
+
for i in range(num_markers):
|
|
36
|
+
position = struct.unpack(">f", fp.read(4))[0]
|
|
37
|
+
data = fp.read(4)
|
|
38
|
+
if i == num_markers - 1:
|
|
39
|
+
bpm = struct.unpack(">f", data)[0]
|
|
40
|
+
yield TerminalBeatgridMarker(position, bpm)
|
|
41
|
+
else:
|
|
42
|
+
beats_till_next_marker = struct.unpack(">I", data)[0]
|
|
43
|
+
yield NonTerminalBeatgridMarker(position, beats_till_next_marker)
|
|
44
|
+
|
|
45
|
+
# TODO: What's the meaning of the footer byte?
|
|
46
|
+
yield Footer(struct.unpack("B", fp.read(1))[0])
|
|
47
|
+
assert fp.read() == b""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
import argparse
|
|
52
|
+
|
|
53
|
+
import mutagen._file
|
|
54
|
+
|
|
55
|
+
from .utils.tags import get_geob
|
|
56
|
+
|
|
57
|
+
parser = argparse.ArgumentParser()
|
|
58
|
+
parser.add_argument("file")
|
|
59
|
+
args = parser.parse_args()
|
|
60
|
+
|
|
61
|
+
tagfile = mutagen._file.File(args.file)
|
|
62
|
+
if tagfile is not None:
|
|
63
|
+
fp = io.BytesIO(get_geob(tagfile, GEOB_KEY))
|
|
64
|
+
else:
|
|
65
|
+
fp = open(args.file, mode="rb")
|
|
66
|
+
|
|
67
|
+
with fp:
|
|
68
|
+
for marker in parse(fp):
|
|
69
|
+
print(marker)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
REPLAY_GAIN_GAIN_KEY = "replaygain_SeratoGain_gain"
|
|
4
|
+
REPLAY_GAIN_PEAK_KEY = "replaygain_SeratoGain_peak"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
if __name__ == "__main__":
|
|
8
|
+
import argparse
|
|
9
|
+
|
|
10
|
+
import mutagen._file
|
|
11
|
+
|
|
12
|
+
parser = argparse.ArgumentParser()
|
|
13
|
+
parser.add_argument("file")
|
|
14
|
+
args = parser.parse_args()
|
|
15
|
+
|
|
16
|
+
tagfile = mutagen._file.File(args.file)
|
|
17
|
+
if tagfile is not None:
|
|
18
|
+
gain = tagfile.get(REPLAY_GAIN_GAIN_KEY, None)
|
|
19
|
+
peak = tagfile.get(REPLAY_GAIN_PEAK_KEY, None)
|
|
20
|
+
print(f"gain: {gain}")
|
|
21
|
+
print(f"peak: {peak}")
|