osz2 1.1.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.
osz2-1.1.0/LICENSE ADDED
@@ -0,0 +1,8 @@
1
+
2
+ Copyright (c) 2025 Levi <contact@lekuru.xyz>, ascenttree
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5
+
6
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7
+
8
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
osz2-1.1.0/PKG-INFO ADDED
@@ -0,0 +1,174 @@
1
+ Metadata-Version: 2.4
2
+ Name: osz2
3
+ Version: 1.1.0
4
+ Summary: A python library for reading osz2 files
5
+ Author: ascenttree
6
+ Author-email: Lekuru <contact@lekuru.xyz>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/Lekuruu/osz2.py
9
+ Keywords: osu,osz2,python,bancho
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: numpy
14
+ Requires-Dist: bsdiff4
15
+ Dynamic: license-file
16
+
17
+ # osz2.py
18
+
19
+ [![Python Version](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/)
20
+ [![GitHub License](https://img.shields.io/github/license/Lekuruu/osz2.py)](https://github.com/Lekuruu/osz2.py/blob/main/LICENSE)
21
+ [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/Lekuruu/osz2.py/.github%2Fworkflows%2Fbuild.yml)](https://github.com/Lekuruu/osz2.py/actions/workflows/build.yml)
22
+
23
+ osz2.py is a Python library for reading osz2 files. It's a direct port of the existing [Osz2Decryptor](https://github.com/xxCherry/Osz2Decryptor) project by [xxCherry](https://github.com/xxCherry) and [osz2-go](https://github.com/Lekuruu/osz2-go) by me. The Python port itself was done by [@ascenttree](https://github.com/ascenttree); all credit goes to them. I took part in code refactoring and optimizing the performance by moving the heavy crypto primitives into a native C extension (`osz2.crypto`) on top of [NumPy](https://numpy.org/), bringing encryption time down to ~100 ms instead of 25 seconds.
24
+
25
+ This project *won't* provide beatmap parsing support. You will have to implement that by yourself, if you decide to use this library for implementing the beatmap submission system.
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ pip install osz2
31
+ ```
32
+
33
+ Or install from source:
34
+
35
+ ```bash
36
+ git clone https://github.com/Lekuruu/osz2.py
37
+ cd osz2.py
38
+ pip install -e .
39
+ ```
40
+
41
+ ## Usage
42
+
43
+ This repository provides a command-line interface for easy testing:
44
+
45
+ ```bash
46
+ python -m osz2 decrypt <input.osz2> <output_directory>
47
+ ```
48
+
49
+ ```bash
50
+ python -m osz2 encrypt <target_directory> <output.osz2>
51
+ ```
52
+
53
+ But that's not all!
54
+ Here is an example of how to use osz2.py as a library:
55
+
56
+ ```python
57
+ from osz2 import Osz2Package, MetadataType
58
+
59
+ # Parse package from file
60
+ package = Osz2Package.from_file("beatmap.osz2")
61
+
62
+ # Access metadata
63
+ print("Title:", package.metadata.get(MetadataType.Title))
64
+ print("Artist:", package.metadata.get(MetadataType.Artist))
65
+ print("Creator:", package.metadata.get(MetadataType.Creator))
66
+ print("Difficulty:", package.metadata.get(MetadataType.Difficulty))
67
+
68
+ # Access files
69
+ for file in package.files:
70
+ print(f"File: {file.filename}, Size: {len(file.content)} bytes")
71
+
72
+ # Extract specific files
73
+ for file in package.files:
74
+ if not file.filename.endswith(".osu"):
75
+ continue
76
+
77
+ with open(file.filename, "wb") as f:
78
+ f.write(file.content)
79
+
80
+ # Create a regular .osz package
81
+ with open("beatmap.osz", "wb") as f:
82
+ f.write(package.create_osz_package())
83
+ ```
84
+
85
+ ### Metadata-only Mode
86
+
87
+ If you only need to read metadata without extracting files, you can use the `metadata_only` parameter:
88
+
89
+ ```python
90
+ # Only parse metadata
91
+ package = Osz2Package.from_file("beatmap.osz2", metadata_only=True)
92
+
93
+ # Access metadata
94
+ print("Title:", package.metadata.get(MetadataType.Title))
95
+ print("BeatmapSet ID:", package.metadata.get(MetadataType.BeatmapSetID))
96
+ ```
97
+
98
+ ### Alternative Constructors
99
+
100
+ ```python
101
+ # From file path
102
+ package = Osz2Package.from_file("beatmap.osz2")
103
+
104
+ # From bytes
105
+ with open("beatmap.osz2", "rb") as f:
106
+ data = f.read()
107
+ package = Osz2Package.from_bytes(data)
108
+
109
+ # From an io.BufferedReader-like object, e.g. a file stream
110
+ with open("beatmap.osz2", "rb") as f:
111
+ package = Osz2Package(f)
112
+ ```
113
+
114
+ ### Exporting an osz2 package
115
+
116
+ You can initialize and export osz2 packages from a directory:
117
+
118
+ ```python
119
+ from osz2 import Osz2Package, MetadataType
120
+
121
+ # Initialize package from a directory containing beatmap files
122
+ package = Osz2Package.from_directory("./my_beatmap_folder")
123
+
124
+ # Export to osz2 format
125
+ osz2_data = package.export()
126
+
127
+ with open("output.osz2", "wb") as f:
128
+ f.write(osz2_data)
129
+ ```
130
+
131
+ ### Applying a patch
132
+
133
+ When developing an implementation of the beatmap submission system, this could come in handy:
134
+
135
+ ```python
136
+ # Assuming you have a source osz2 file and a patch file
137
+ osz2_file = b"..."
138
+ patch_file = b"..."
139
+
140
+ updated_osz2 = osz2.apply_bsdiff_patch(osz2_file, patch_file)
141
+ osz2 = Osz2Package.from_bytes(updated_osz2)
142
+ ```
143
+
144
+ ### Using osu!stream .osf2 files
145
+
146
+ I have not tested this, but in theory this should work by passing in `KeyType.OSF2` when initializing the osz2 package:
147
+
148
+ ```python
149
+ osf2 = Osz2Package.from_file("beatmap.osf2", key_type=KeyType.OSF2)
150
+ ```
151
+
152
+ You can also specify this when using the command-line interface:
153
+
154
+ ```bash
155
+ python -m osz2 <input.osz2> <output_directory> --key-type osf2
156
+ ```
157
+
158
+ ## Building the crypto.c extension
159
+
160
+ If you change any code under `osz2/crypto.c`, rebuild the module in place before running tests:
161
+
162
+ ```bash
163
+ python -m venv venv
164
+ source venv/bin/activate
165
+ pip install -e .
166
+ ```
167
+
168
+ Or, if you only need the extension locally:
169
+
170
+ ```bash
171
+ python setup.py build_ext --inplace
172
+ ```
173
+
174
+ The resulting shared object is picked up automatically by the package import system.
osz2-1.1.0/README.md ADDED
@@ -0,0 +1,158 @@
1
+ # osz2.py
2
+
3
+ [![Python Version](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/)
4
+ [![GitHub License](https://img.shields.io/github/license/Lekuruu/osz2.py)](https://github.com/Lekuruu/osz2.py/blob/main/LICENSE)
5
+ [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/Lekuruu/osz2.py/.github%2Fworkflows%2Fbuild.yml)](https://github.com/Lekuruu/osz2.py/actions/workflows/build.yml)
6
+
7
+ osz2.py is a Python library for reading osz2 files. It's a direct port of the existing [Osz2Decryptor](https://github.com/xxCherry/Osz2Decryptor) project by [xxCherry](https://github.com/xxCherry) and [osz2-go](https://github.com/Lekuruu/osz2-go) by me. The Python port itself was done by [@ascenttree](https://github.com/ascenttree); all credit goes to them. I took part in code refactoring and optimizing the performance by moving the heavy crypto primitives into a native C extension (`osz2.crypto`) on top of [NumPy](https://numpy.org/), bringing encryption time down to ~100 ms instead of 25 seconds.
8
+
9
+ This project *won't* provide beatmap parsing support. You will have to implement that by yourself, if you decide to use this library for implementing the beatmap submission system.
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ pip install osz2
15
+ ```
16
+
17
+ Or install from source:
18
+
19
+ ```bash
20
+ git clone https://github.com/Lekuruu/osz2.py
21
+ cd osz2.py
22
+ pip install -e .
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ This repository provides a command-line interface for easy testing:
28
+
29
+ ```bash
30
+ python -m osz2 decrypt <input.osz2> <output_directory>
31
+ ```
32
+
33
+ ```bash
34
+ python -m osz2 encrypt <target_directory> <output.osz2>
35
+ ```
36
+
37
+ But that's not all!
38
+ Here is an example of how to use osz2.py as a library:
39
+
40
+ ```python
41
+ from osz2 import Osz2Package, MetadataType
42
+
43
+ # Parse package from file
44
+ package = Osz2Package.from_file("beatmap.osz2")
45
+
46
+ # Access metadata
47
+ print("Title:", package.metadata.get(MetadataType.Title))
48
+ print("Artist:", package.metadata.get(MetadataType.Artist))
49
+ print("Creator:", package.metadata.get(MetadataType.Creator))
50
+ print("Difficulty:", package.metadata.get(MetadataType.Difficulty))
51
+
52
+ # Access files
53
+ for file in package.files:
54
+ print(f"File: {file.filename}, Size: {len(file.content)} bytes")
55
+
56
+ # Extract specific files
57
+ for file in package.files:
58
+ if not file.filename.endswith(".osu"):
59
+ continue
60
+
61
+ with open(file.filename, "wb") as f:
62
+ f.write(file.content)
63
+
64
+ # Create a regular .osz package
65
+ with open("beatmap.osz", "wb") as f:
66
+ f.write(package.create_osz_package())
67
+ ```
68
+
69
+ ### Metadata-only Mode
70
+
71
+ If you only need to read metadata without extracting files, you can use the `metadata_only` parameter:
72
+
73
+ ```python
74
+ # Only parse metadata
75
+ package = Osz2Package.from_file("beatmap.osz2", metadata_only=True)
76
+
77
+ # Access metadata
78
+ print("Title:", package.metadata.get(MetadataType.Title))
79
+ print("BeatmapSet ID:", package.metadata.get(MetadataType.BeatmapSetID))
80
+ ```
81
+
82
+ ### Alternative Constructors
83
+
84
+ ```python
85
+ # From file path
86
+ package = Osz2Package.from_file("beatmap.osz2")
87
+
88
+ # From bytes
89
+ with open("beatmap.osz2", "rb") as f:
90
+ data = f.read()
91
+ package = Osz2Package.from_bytes(data)
92
+
93
+ # From an io.BufferedReader-like object, e.g. a file stream
94
+ with open("beatmap.osz2", "rb") as f:
95
+ package = Osz2Package(f)
96
+ ```
97
+
98
+ ### Exporting an osz2 package
99
+
100
+ You can initialize and export osz2 packages from a directory:
101
+
102
+ ```python
103
+ from osz2 import Osz2Package, MetadataType
104
+
105
+ # Initialize package from a directory containing beatmap files
106
+ package = Osz2Package.from_directory("./my_beatmap_folder")
107
+
108
+ # Export to osz2 format
109
+ osz2_data = package.export()
110
+
111
+ with open("output.osz2", "wb") as f:
112
+ f.write(osz2_data)
113
+ ```
114
+
115
+ ### Applying a patch
116
+
117
+ When developing an implementation of the beatmap submission system, this could come in handy:
118
+
119
+ ```python
120
+ # Assuming you have a source osz2 file and a patch file
121
+ osz2_file = b"..."
122
+ patch_file = b"..."
123
+
124
+ updated_osz2 = osz2.apply_bsdiff_patch(osz2_file, patch_file)
125
+ osz2 = Osz2Package.from_bytes(updated_osz2)
126
+ ```
127
+
128
+ ### Using osu!stream .osf2 files
129
+
130
+ I have not tested this, but in theory this should work by passing in `KeyType.OSF2` when initializing the osz2 package:
131
+
132
+ ```python
133
+ osf2 = Osz2Package.from_file("beatmap.osf2", key_type=KeyType.OSF2)
134
+ ```
135
+
136
+ You can also specify this when using the command-line interface:
137
+
138
+ ```bash
139
+ python -m osz2 <input.osz2> <output_directory> --key-type osf2
140
+ ```
141
+
142
+ ## Building the crypto.c extension
143
+
144
+ If you change any code under `osz2/crypto.c`, rebuild the module in place before running tests:
145
+
146
+ ```bash
147
+ python -m venv venv
148
+ source venv/bin/activate
149
+ pip install -e .
150
+ ```
151
+
152
+ Or, if you only need the extension locally:
153
+
154
+ ```bash
155
+ python setup.py build_ext --inplace
156
+ ```
157
+
158
+ The resulting shared object is picked up automatically by the package import system.
@@ -0,0 +1,17 @@
1
+
2
+ __author__ = "Lekuru"
3
+ __email__ = "contact@lekuru.xyz"
4
+ __version__ = "1.1.0"
5
+ __license__ = "MIT"
6
+
7
+ from .patch import apply_bsdiff_patch
8
+ from .metadata import MetadataType
9
+ from .package import Osz2Package
10
+ from .keys import KeyType
11
+ from .file import File
12
+
13
+ from .simple_cryptor import SimpleCryptor
14
+ from .xxtea_reader import XXTEAReader
15
+ from .xxtea_writer import XXTEAWriter
16
+ from .xxtea import XXTEA
17
+ from .xtea import XTEA
@@ -0,0 +1,186 @@
1
+
2
+ from typing import Tuple, Dict, Union, Optional
3
+ from osz2.metadata import MetadataType
4
+ from osz2.package import Osz2Package
5
+ from osz2.keys import KeyType
6
+ from pathlib import Path
7
+
8
+ import argparse
9
+ import sys
10
+ import os
11
+
12
+ def main() -> None:
13
+ parser = argparse.ArgumentParser(prog="osz2", description="A tool to decrypt, extract, and create osz2 files")
14
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
15
+
16
+ decrypt_parser = subparsers.add_parser("decrypt", help="Decrypt and extract an osz2 file")
17
+ decrypt_parser.add_argument("input", help="The path to the osz2 file to decrypt")
18
+ decrypt_parser.add_argument("output", help="The path to put the extracted files")
19
+ decrypt_parser.add_argument("--key-type", choices=["osz2", "osf2"], default="osz2", help="The key generation method")
20
+ decrypt_parser.add_argument("--create-osz", action="store_true", help="Also create a regular .osz package")
21
+
22
+ encrypt_parser = subparsers.add_parser("encrypt", help="Create an osz2 package from a directory")
23
+ encrypt_parser.add_argument("input", help="The path to the directory containing files")
24
+ encrypt_parser.add_argument("output", help="The output path for the osz2 file")
25
+ encrypt_parser.add_argument("--key-type", choices=["osz2", "osf2"], default="osz2", help="The key generation method")
26
+
27
+ args = parser.parse_args()
28
+
29
+ if args.command is None:
30
+ parser.print_help()
31
+ return
32
+
33
+ key_type = KeyType(args.key_type)
34
+
35
+ if args.command == "decrypt":
36
+ osz2 = decrypt_osz2(args.input, key_type)
37
+ save_osz2(osz2, args.output)
38
+
39
+ if not args.create_osz:
40
+ return
41
+
42
+ osz_data = osz2.create_osz_package(exclude_disallowed_files=False)
43
+ with open(f"{args.output}/{osz2.osz_filename}", "wb") as f:
44
+ f.write(osz_data)
45
+
46
+ elif args.command == "encrypt":
47
+ encrypt_directory(
48
+ args.input,
49
+ args.output,
50
+ key_type
51
+ )
52
+
53
+ def decrypt_osz2(filepath: str, key_type: KeyType) -> Osz2Package:
54
+ if not os.path.exists(filepath):
55
+ print(f"Error: Input file does not exist: {filepath}", file=sys.stderr)
56
+ sys.exit(1)
57
+
58
+ print("Reading osz2 package...")
59
+ return Osz2Package.from_file(filepath, key_type=key_type)
60
+
61
+ def save_osz2(package: Osz2Package, output: str) -> None:
62
+ Path(output).mkdir(exist_ok=True)
63
+ print(f"Extracting {len(package.files)} files to {output}")
64
+
65
+ for file in package.files:
66
+ output_path = os.path.join(output, file.filename)
67
+
68
+ if (dir := Path(output_path).parent) != ".":
69
+ dir.mkdir(parents=True, exist_ok=True)
70
+
71
+ with open(output_path, "wb") as f:
72
+ f.write(file.content)
73
+
74
+ print(f" -> {file.filename} ({len(file.content)} bytes)")
75
+
76
+ def encrypt_directory(
77
+ directory: str,
78
+ output: str,
79
+ key_type: KeyType
80
+ ) -> None:
81
+ if not os.path.exists(directory):
82
+ print(f"Error: Input directory does not exist: {directory}", file=sys.stderr)
83
+ sys.exit(1)
84
+
85
+ print(f"Creating osz2 package from directory: {directory}")
86
+ package = Osz2Package.from_directory(directory, key_type)
87
+
88
+ # Try to parse beatmap metadata and apply it to package
89
+ for beatmap in package.beatmap_files:
90
+ _, content = parse_beatmap(beatmap.content.decode("utf-8-sig"))
91
+ beatmap_id = content.get('Metadata', {}).get('BeatmapID', None)
92
+
93
+ if beatmap_id is not None:
94
+ package.beatmap_ids[beatmap.filename] = int(beatmap_id)
95
+
96
+ apply_metadata(package, content)
97
+
98
+ print(f"Exporting package with {len(package.files)} files...")
99
+
100
+ with open(output, "wb") as f:
101
+ data = package.export()
102
+ f.write(data)
103
+
104
+ print(f"Saved to: {output}")
105
+
106
+ def apply_metadata(package: Osz2Package, beatmap: Dict[str, dict]) -> None:
107
+ if 'Metadata' not in beatmap:
108
+ print("Error: No 'Metadata' section found in beatmap")
109
+ sys.exit(1)
110
+
111
+ if 'General' not in beatmap:
112
+ print("Error: No 'General' section found in beatmap")
113
+ sys.exit(1)
114
+
115
+ metadata_section = beatmap['Metadata']
116
+ title = metadata_section.get('TitleUnicode') or metadata_section.get('Title', '')
117
+ artist = metadata_section.get('ArtistUnicode') or metadata_section.get('Artist', '')
118
+ creator = metadata_section.get('Creator', '')
119
+ source = metadata_section.get('Source', '')
120
+ tags = metadata_section.get('Tags', '')
121
+ beatmapset_id = metadata_section.get('BeatmapSetID', -1)
122
+
123
+ general_section = beatmap['General']
124
+ preview_time = general_section.get('PreviewTime', 0)
125
+
126
+ package.metadata[MetadataType.Title] = str(title)
127
+ package.metadata[MetadataType.Artist] = str(artist)
128
+ package.metadata[MetadataType.Creator] = str(creator)
129
+ package.metadata[MetadataType.Source] = str(source)
130
+ package.metadata[MetadataType.Tags] = str(tags)
131
+ package.metadata[MetadataType.BeatmapSetID] = str(beatmapset_id)
132
+ package.metadata[MetadataType.PreviewTime] = str(preview_time)
133
+
134
+ def parse_beatmap(content: str) -> Tuple[int, Dict[str, dict]]:
135
+ sections: Dict[str, Union[dict, list]] = {}
136
+ current_section = None
137
+ beatmap_version = 0
138
+
139
+ for line in content.splitlines():
140
+ if line.startswith('osu file format'):
141
+ beatmap_version = int(line.removeprefix('osu file format v'))
142
+ continue
143
+
144
+ if (line.startswith('[') and line.endswith(']')):
145
+ # New section
146
+ current_section = line.removeprefix('[').removesuffix(']')
147
+ continue
148
+
149
+ if current_section is None:
150
+ continue
151
+
152
+ if not line:
153
+ continue
154
+
155
+ if current_section in ('General', 'Editor', 'Metadata', 'Difficulty'):
156
+ if current_section not in sections:
157
+ sections[current_section] = {}
158
+
159
+ # Parse key, value pair
160
+ key, value = (
161
+ split.strip() for split in line.split(':', maxsplit=1)
162
+ )
163
+
164
+ # Try to parse float/int
165
+ value = parse_number(value) or value
166
+
167
+ sections[current_section][key] = value
168
+ continue
169
+
170
+ if current_section not in sections:
171
+ sections[current_section] = []
172
+
173
+ # Append to list
174
+ sections[current_section].append(line)
175
+
176
+ return beatmap_version, sections
177
+
178
+ def parse_number(value: str) -> Optional[Union[int, float]]:
179
+ for cast in (int, float):
180
+ try:
181
+ return cast(value.strip())
182
+ except ValueError:
183
+ continue
184
+
185
+ if __name__ == "__main__":
186
+ main()
@@ -0,0 +1,29 @@
1
+
2
+ # "knownPlain" constant derived from FastRandom(1990)
3
+ # https://github.com/ppy/osu-stream/blob/master/osu!stream/Helpers/osu!common/MapPackage.cs#L64
4
+ KNOWN_PLAIN = bytearray([
5
+ 0x55, 0xAA, 0x74, 0x10, 0x2B, 0x56, 0xB3, 0x9E,
6
+ 0x25, 0x9E, 0xFE, 0xB7, 0xBE, 0x06, 0xFC, 0xF2,
7
+ 0xB6, 0x3C, 0x6F, 0x47, 0x7E, 0x38, 0x69, 0x43,
8
+ 0x80, 0x89, 0x25, 0x00, 0xCC, 0xB6, 0xFE, 0x12,
9
+ 0xA9, 0xB2, 0x4A, 0x2C, 0x96, 0xD5, 0xEA, 0x26,
10
+ 0x42, 0x31, 0xAF, 0x0A, 0x0D, 0xAE, 0x00, 0xED,
11
+ 0xFE, 0x96, 0xA6, 0x94, 0x99, 0xA7, 0x90, 0xE4,
12
+ 0x68, 0xBF, 0xC6, 0x97, 0x5B, 0x1B, 0x5E, 0x7F
13
+ ])
14
+
15
+ # A list of all allowed file extensions in an .osz package
16
+ # Did not cause any issues when testing on titanic
17
+ ALLOWED_FILE_EXTENSIONS = (
18
+ "osu", "osz", "osb", "osk", "png", "mp3",
19
+ "wav", "ogg", "jpg", "wmv", "flv", "flac",
20
+ "avi", "ini", "m4v", "mpg", "mov", "webm",
21
+ "ogv", "mpeg", "3gp", "mkv", "mp4", "jpeg",
22
+ )
23
+
24
+ # osu! officially only uses ".avi", ".flv" and ".mpg" for
25
+ # video files, so lets hope this won't cause any issues
26
+ VIDEO_FILE_EXTENSIONS = (
27
+ "wmv", "flv", "avi", "m4v", "mpg", "mov",
28
+ "webm", "ogv", "mpeg", "3gp", "mkv", "mp4",
29
+ )