├── .flake8
├── .gitattributes
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ └── pythonapp.yml
├── .gitignore
├── LICENSE
├── README.md
├── docs
├── index.html
├── pycco.css
├── qua2osu-gui.html
└── qua2osu.html
├── gui
├── gui.py
└── gui.ui
├── qua2osu-gui.py
├── qua2osu.py
└── requirements.txt
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | exclude =
3 | # Autogenerated file
4 | gui.py,
5 | build,dist,docs,env
6 |
7 | # star imports
8 | ignore = F403,F405
9 | # because 79 is just too low
10 | max-line-length = 125
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug
6 | assignees: IceDynamix
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Additional context**
32 | Add any other context about the problem here.
33 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: enhancement
6 | assignees: IceDynamix
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/workflows/pythonapp.yml:
--------------------------------------------------------------------------------
1 | name: Python application
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: ubuntu-latest
9 |
10 | steps:
11 | - uses: actions/checkout@v1
12 | - name: Set up Python 3.7
13 | uses: actions/setup-python@v1
14 | with:
15 | python-version: 3.7
16 | - name: Install dependencies
17 | run: |
18 | python -m pip install --upgrade pip
19 | pip install -r requirements.txt
20 | - name: Lint with flake8
21 | run: |
22 | pip install flake8
23 | # stop the build if there are Python syntax errors or undefined names
24 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
25 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
26 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # celery beat schedule file
95 | celerybeat-schedule
96 |
97 | # SageMath parsed files
98 | *.sage.py
99 |
100 | # Environments
101 | .env
102 | .venv
103 | env/
104 | venv/
105 | ENV/
106 | env.bak/
107 | venv.bak/
108 |
109 | # Spyder project settings
110 | .spyderproject
111 | .spyproject
112 |
113 | # Rope project settings
114 | .ropeproject
115 |
116 | # mkdocs documentation
117 | /site
118 |
119 | # mypy
120 | .mypy_cache/
121 | .dmypy.json
122 | dmypy.json
123 |
124 | # Pyre type checker
125 | .pyre/
126 | .vscode/settings.json
127 |
128 | # Custom
129 | output/
130 | input/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 IceDynamix
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # qua2osu
2 |
3 | > Converts quaver map files (.qp) to osu! map files (.osz)
4 |
5 | Outdated and not maintained anymore, use my rewrite [qua3osu](https://github.com/IceDynamix/qua3osu) instead. Currently doesn't have a GUI yet, but saves a lot of hassle.
6 |
7 |
8 | Old content
9 |
10 | ## Screenshot
11 |
12 | 
13 |
14 | ## Download
15 |
16 | Download the lastest release from
17 | [here](https://github.com/IceDynamix/qua2osu/releases)
18 |
19 | ## Step by step instructions
20 |
21 | - Download qua2osu, no installation needed etc.
22 | - You can decide to use the command-line tool or the GUI (Graphical User
23 | Interface)
24 | - Command-line:
25 | - Execute the qua2osu.exe file with the --help flag in the console by
26 | running `qua2osu.exe --help` (double-clicking doesn't work)
27 | - Set options with some flags to spice things up (Example:
28 | `qua2osu.exe -i myfolder/subfolder -od 8.5 -hp 9 -hv 0` which would
29 | convert all .qp files in "myfolder/subfolder", set OD to 8.5, HP to 9 and
30 | hitsound volume to 0)
31 | - GUI:
32 | - Execute the qua2osu-gui.exe file, either by double-clicking or by running
33 | it in the console
34 | - GUI should pop up, select a folder with your .qp files and select a folder
35 | to output your .osz files
36 | - Set some settings and click on convert
37 |
38 | ## Step by step instructions to build the project yourself
39 |
40 | - Install [Git](https://git-scm.com/) and [Python](https://www.python.org/) if
41 | necessary
42 | - Install [pip](https://pip.pypa.io/en/stable/installing/) if necessary (should
43 | ship with python)
44 | - Clone this repo: `git clone https://github.com/IceDynamix/qua2osu.git`
45 | - *It's best to set up a
46 | [virtual environment](https://docs.python.org/3/tutorial/venv.html) for the
47 | project, but not necessary if you don't know how to*
48 | - *Activate your virtual environment by running the activate file in your
49 | virtual environment folder*
50 | - Run `pip install -r requirements.txt` in the directory to install all package
51 | dependencies (mainly reamber (conversion), PyQT5 (gui) and some QOL stuff)
52 | - Run `py qua2osu.py` or `py qua2osu-gui.py`
53 |
54 | ## Documentation
55 |
56 | This project uses [pycco](https://github.com/pycco-docs/pycco) to create
57 | documentation. Regenerating the documentation after modifying or adding new
58 | files is done by `pycco ./*.py`. Having a git hook that generates it pre-commit
59 | and adds it to the staged files is recommended.
60 |
61 | [Documentation](https://icedynamix.github.io/qua2osu/index.html)
62 |
63 | ## Contributing
64 |
65 | In case you want to contribute to this project, please keep following things in
66 | mind:
67 |
68 | - This project uses [flake8](http://flake8.pycqa.org/en/latest/) as the primary
69 | linter. A `.flake8` is present in the root directory. Run
70 | `flake8 yourpythonfile.py` to lint your file.
71 | - [QT Designer](https://build-system.fman.io/qt-designer-download) is used to
72 | work with the gui.ui file. Use `pyuic5 -x gui/gui.ui -o gui/gui.py` in the
73 | root directory to regenerate the gui.py file after editing the gui.ui file.
74 | - Use [camelCase](https://en.wikipedia.org/wiki/Camel_case) for variable,
75 | function and method names.
76 | - Use PascalCase (camelCase, but first letter capitalized) for class names.
77 | - Use UPPER_SNAKE_CASE for constants.
78 | - Please document your code. Refer to the Documentation section.
79 |
80 | Please report issues [here on github](https://github.com/IceDynamix/qua2osu/issues).
81 |
82 | ## Referenced games
83 |
84 | - [**Quaver**](https://quavergame.com/)
85 | - [**osu!**](https://osu.ppy.sh/)
86 |
87 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
14 |
28 |
29 |
--------------------------------------------------------------------------------
/docs/pycco.css:
--------------------------------------------------------------------------------
1 | /*--------------------- Layout and Typography ----------------------------*/
2 | body {
3 | font-family: 'Palatino Linotype', 'Book Antiqua', Palatino, FreeSerif, serif;
4 | font-size: 16px;
5 | line-height: 24px;
6 | color: #252519;
7 | margin: 0; padding: 0;
8 | background: #f5f5ff;
9 | }
10 | a {
11 | color: #261a3b;
12 | }
13 | a:visited {
14 | color: #261a3b;
15 | }
16 | p {
17 | margin: 0 0 15px 0;
18 | }
19 | h1, h2, h3, h4, h5, h6 {
20 | margin: 40px 0 15px 0;
21 | }
22 | h2, h3, h4, h5, h6 {
23 | margin-top: 0;
24 | }
25 | #container {
26 | background: white;
27 | }
28 | #container, div.section {
29 | position: relative;
30 | }
31 | #background {
32 | position: absolute;
33 | top: 0; left: 580px; right: 0; bottom: 0;
34 | background: #f5f5ff;
35 | border-left: 1px solid #e5e5ee;
36 | z-index: 0;
37 | }
38 | #jump_to, #jump_page {
39 | background: white;
40 | -webkit-box-shadow: 0 0 25px #777; -moz-box-shadow: 0 0 25px #777;
41 | -webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px;
42 | font: 10px Arial;
43 | text-transform: uppercase;
44 | cursor: pointer;
45 | text-align: right;
46 | }
47 | #jump_to, #jump_wrapper {
48 | position: fixed;
49 | right: 0; top: 0;
50 | padding: 5px 10px;
51 | }
52 | #jump_wrapper {
53 | padding: 0;
54 | display: none;
55 | }
56 | #jump_to:hover #jump_wrapper {
57 | display: block;
58 | }
59 | #jump_page {
60 | padding: 5px 0 3px;
61 | margin: 0 0 25px 25px;
62 | }
63 | #jump_page .source {
64 | display: block;
65 | padding: 5px 10px;
66 | text-decoration: none;
67 | border-top: 1px solid #eee;
68 | }
69 | #jump_page .source:hover {
70 | background: #f5f5ff;
71 | }
72 | #jump_page .source:first-child {
73 | }
74 | div.docs {
75 | float: left;
76 | max-width: 500px;
77 | min-width: 500px;
78 | min-height: 5px;
79 | padding: 10px 25px 1px 50px;
80 | vertical-align: top;
81 | text-align: left;
82 | }
83 | .docs pre {
84 | margin: 15px 0 15px;
85 | padding-left: 15px;
86 | }
87 | .docs p tt, .docs p code {
88 | background: #f8f8ff;
89 | border: 1px solid #dedede;
90 | font-size: 12px;
91 | padding: 0 0.2em;
92 | }
93 | .octowrap {
94 | position: relative;
95 | }
96 | .octothorpe {
97 | font: 12px Arial;
98 | text-decoration: none;
99 | color: #454545;
100 | position: absolute;
101 | top: 3px; left: -20px;
102 | padding: 1px 2px;
103 | opacity: 0;
104 | -webkit-transition: opacity 0.2s linear;
105 | }
106 | div.docs:hover .octothorpe {
107 | opacity: 1;
108 | }
109 | div.code {
110 | margin-left: 580px;
111 | padding: 14px 15px 16px 50px;
112 | vertical-align: top;
113 | }
114 | .code pre, .docs p code {
115 | font-size: 12px;
116 | }
117 | pre, tt, code {
118 | line-height: 18px;
119 | font-family: Monaco, Consolas, "Lucida Console", monospace;
120 | margin: 0; padding: 0;
121 | }
122 | div.clearall {
123 | clear: both;
124 | }
125 |
126 |
127 | /*---------------------- Syntax Highlighting -----------------------------*/
128 | td.linenos { background-color: #f0f0f0; padding-right: 10px; }
129 | span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; }
130 | body .hll { background-color: #ffffcc }
131 | body .c { color: #408080; font-style: italic } /* Comment */
132 | body .err { border: 1px solid #FF0000 } /* Error */
133 | body .k { color: #954121 } /* Keyword */
134 | body .o { color: #666666 } /* Operator */
135 | body .cm { color: #408080; font-style: italic } /* Comment.Multiline */
136 | body .cp { color: #BC7A00 } /* Comment.Preproc */
137 | body .c1 { color: #408080; font-style: italic } /* Comment.Single */
138 | body .cs { color: #408080; font-style: italic } /* Comment.Special */
139 | body .gd { color: #A00000 } /* Generic.Deleted */
140 | body .ge { font-style: italic } /* Generic.Emph */
141 | body .gr { color: #FF0000 } /* Generic.Error */
142 | body .gh { color: #000080; font-weight: bold } /* Generic.Heading */
143 | body .gi { color: #00A000 } /* Generic.Inserted */
144 | body .go { color: #808080 } /* Generic.Output */
145 | body .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
146 | body .gs { font-weight: bold } /* Generic.Strong */
147 | body .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
148 | body .gt { color: #0040D0 } /* Generic.Traceback */
149 | body .kc { color: #954121 } /* Keyword.Constant */
150 | body .kd { color: #954121; font-weight: bold } /* Keyword.Declaration */
151 | body .kn { color: #954121; font-weight: bold } /* Keyword.Namespace */
152 | body .kp { color: #954121 } /* Keyword.Pseudo */
153 | body .kr { color: #954121; font-weight: bold } /* Keyword.Reserved */
154 | body .kt { color: #B00040 } /* Keyword.Type */
155 | body .m { color: #666666 } /* Literal.Number */
156 | body .s { color: #219161 } /* Literal.String */
157 | body .na { color: #7D9029 } /* Name.Attribute */
158 | body .nb { color: #954121 } /* Name.Builtin */
159 | body .nc { color: #0000FF; font-weight: bold } /* Name.Class */
160 | body .no { color: #880000 } /* Name.Constant */
161 | body .nd { color: #AA22FF } /* Name.Decorator */
162 | body .ni { color: #999999; font-weight: bold } /* Name.Entity */
163 | body .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
164 | body .nf { color: #0000FF } /* Name.Function */
165 | body .nl { color: #A0A000 } /* Name.Label */
166 | body .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
167 | body .nt { color: #954121; font-weight: bold } /* Name.Tag */
168 | body .nv { color: #19469D } /* Name.Variable */
169 | body .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
170 | body .w { color: #bbbbbb } /* Text.Whitespace */
171 | body .mf { color: #666666 } /* Literal.Number.Float */
172 | body .mh { color: #666666 } /* Literal.Number.Hex */
173 | body .mi { color: #666666 } /* Literal.Number.Integer */
174 | body .mo { color: #666666 } /* Literal.Number.Oct */
175 | body .sb { color: #219161 } /* Literal.String.Backtick */
176 | body .sc { color: #219161 } /* Literal.String.Char */
177 | body .sd { color: #219161; font-style: italic } /* Literal.String.Doc */
178 | body .s2 { color: #219161 } /* Literal.String.Double */
179 | body .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
180 | body .sh { color: #219161 } /* Literal.String.Heredoc */
181 | body .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
182 | body .sx { color: #954121 } /* Literal.String.Other */
183 | body .sr { color: #BB6688 } /* Literal.String.Regex */
184 | body .s1 { color: #219161 } /* Literal.String.Single */
185 | body .ss { color: #19469D } /* Literal.String.Symbol */
186 | body .bp { color: #954121 } /* Name.Builtin.Pseudo */
187 | body .vc { color: #19469D } /* Name.Variable.Class */
188 | body .vg { color: #19469D } /* Name.Variable.Global */
189 | body .vi { color: #19469D } /* Name.Variable.Instance */
190 | body .il { color: #666666 } /* Literal.Number.Integer.Long */
191 |
--------------------------------------------------------------------------------
/docs/qua2osu-gui.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
qua2osu-gui.py
6 |
7 |
8 |
9 |
10 |
11 |
14 |
15 |
16 |
22 |
23 |
import os
24 | import sys
25 | import time
26 | import webbrowser # to open the explorer cross-platform
27 |
28 | from PyQt5.QtCore import *
29 | from PyQt5.QtGui import *
30 | from PyQt5.QtWidgets import *
31 |
32 | from conversion import convertMapset
33 |
34 |
35 |
36 |
37 |
38 |
41 |
gui.py is autogenerated from gui.ui (qt designer)
42 |
43 |
46 |
47 |
48 |
49 |
50 |
53 |
use “pyuic5 -x gui/gui.ui -o gui/gui.py” after editing gui.ui with qt designer
54 |
55 |
56 |
from gui.gui import Ui_MainWindow
57 |
58 |
59 |
60 |
61 |
62 |
65 |
Custom window class with all of the GUI functionality
66 |
67 |
68 |
class IceMainWindow ( QMainWindow , Ui_MainWindow ):
69 |
70 |
71 |
72 |
73 |
74 |
77 |
All of the objects themselves are managed by QT designer in the gui.ui (gui.py) file
78 |
79 |
82 |
83 |
84 |
85 |
91 |
92 |
def __init__ ( self ):
93 | QMainWindow . __init__ ( self )
94 | self . setupUi ( self )
95 | self . inputToolButton . clicked . connect ( self . openInputDirectoryDialog )
96 | self . outputToolButton . clicked . connect ( self . openOutputDirectoryDialog )
97 | self . convertPushButton . clicked . connect ( self . convertOnClick )
98 |
99 | self . updateStatus ( "Finished setup" )
100 |
101 |
102 |
103 |
104 |
105 |
108 |
Opens the input directory dialog and sets the input path in the GUI
109 |
110 |
111 |
def openInputDirectoryDialog ( self ):
112 |
113 |
114 |
115 |
116 |
122 |
123 |
path = str ( QFileDialog . getExistingDirectory (
124 | self , "Select Input Folder" ))
125 | self . inputLineEdit . setText ( path )
126 | self . updateStatus ( "Set input path to " + path )
127 |
128 |
129 |
130 |
131 |
132 |
135 |
Opens the output directory dialog and sets the input path in the GUI
136 |
137 |
138 |
def openOutputDirectoryDialog ( self ):
139 |
140 |
141 |
142 |
143 |
149 |
150 |
path = str ( QFileDialog . getExistingDirectory (
151 | self , "Select Output Folder" ))
152 | self . outputLineEdit . setText ( path )
153 | self . updateStatus ( "Set output path to " + path )
154 |
155 |
156 |
157 |
158 |
159 |
162 |
Wrapper for updating the status label, always shows the two last lines
163 |
164 |
165 |
def updateStatus ( self , status ):
166 |
167 |
168 |
169 |
170 |
176 |
177 |
currentText = self . statusLabel . text ()
178 | lastLine = currentText . split ( " \n " )[ - 1 ]
179 | self . statusLabel . setText ( lastLine + " \n " + status )
180 |
181 |
182 |
183 |
184 |
185 |
188 |
Wrapper for updating the progress bar maximum
189 |
190 |
191 |
def updateProgressBarMax ( self , maximum ):
192 |
193 |
194 |
195 |
196 |
202 |
203 |
self . progressBar . setMaximum ( maximum )
204 |
205 |
206 |
207 |
208 |
209 |
212 |
Increments the progress bar by one unit total, adds a smooth animation
213 |
214 |
215 |
def incrementProgressBarValue ( self ):
216 |
217 |
218 |
219 |
220 |
226 |
227 |
self . progressBar . setValue ( self . progressBar . value () + 1 )
228 |
229 |
230 |
231 |
232 |
233 |
236 |
Sets up the converter thread
237 |
238 |
239 |
def initiateConverterThread ( self , inputPath , outputPath , options ):
240 |
241 |
242 |
243 |
244 |
250 |
251 |
converterThread = ConverterThread ( inputPath , outputPath , options )
252 | converterThread . updateStatus . connect ( self . updateStatus )
253 | converterThread . updateProgressbarMax . connect (
254 | self . updateProgressBarMax )
255 | converterThread . incrementProgressbarValue . connect (
256 | self . incrementProgressBarValue )
257 |
258 | return converterThread
259 |
260 |
261 |
262 |
263 |
264 |
267 |
Starts the map conversion of the folder
268 |
269 |
270 |
def convertOnClick ( self ):
271 |
272 |
273 |
274 |
275 |
281 |
282 |
inputPath = self . inputLineEdit . text ()
283 | outputPath = self . outputLineEdit . text ()
284 |
285 | if inputPath == "" or outputPath == "" :
286 | self . updateStatus ( "Empty paths detected, using defaults" )
287 |
288 | if inputPath == "" :
289 | inputPath = "input"
290 |
291 | if outputPath == "" :
292 | outputPath = "output"
293 |
294 | selectedOd = self . odDoubleSpinBox . value ()
295 | selectedHp = self . hpDoubleSpinBox . value ()
296 | selectedHitSoundVolume = self . hsVolumeSpinBox . value ()
297 |
298 |
299 |
300 |
301 |
302 |
305 |
please tell me if there’s an easier way to lookup
306 | which radio button is checked because this is horrible
307 |
308 |
309 |
selectedSampleSet = ""
310 | sampleSetButtons = [
311 | self . sampleSetNormalRadioButton ,
312 | self . sampleSetSoftRadioButton ,
313 | self . sampleSetDrumRadioButton
314 | ]
315 |
316 | for button in sampleSetButtons :
317 | if button . isChecked ():
318 | selectedSampleSet = button . text ()
319 | break
320 |
321 | options = {
322 | "od" : selectedOd ,
323 | "hp" : selectedHp ,
324 | "hitSoundVolume" : selectedHitSoundVolume ,
325 | "sampleSet" : selectedSampleSet
326 | }
327 |
328 | self . progressBar . setValue ( 0 )
329 |
330 | self . converterThread = self . initiateConverterThread (
331 | inputPath , outputPath , options )
332 | self . converterThread . start ()
333 |
334 |
335 |
336 |
337 |
338 |
341 |
Using a different thread for the map conversion
342 |
343 |
344 |
class ConverterThread ( QThread ):
345 |
346 |
347 |
348 |
349 |
350 |
353 |
Otherwise the UI would freeze and become unresponsive during the conversion
354 |
355 |
356 |
updateStatus = pyqtSignal ( str )
357 | incrementProgressbarValue = pyqtSignal ()
358 | updateProgressbarMax = pyqtSignal ( int )
359 |
360 |
361 |
362 |
363 |
369 |
370 |
def __init__ ( self , inputPath , outputPath , options ):
371 | QThread . __init__ ( self )
372 | self . inputPath = inputPath
373 | self . outputPath = outputPath
374 | self . options = options
375 |
376 |
377 |
378 |
379 |
385 |
386 |
def __del__ ( self ):
387 | self . wait ()
388 |
389 |
390 |
391 |
392 |
398 |
399 |
def run ( self ):
400 | qpFilesInInputDir = []
401 |
402 | for file in os . listdir ( self . inputPath ):
403 | path = os . path . join ( self . inputPath , file )
404 | if file . endswith ( '.qp' ) and os . path . isfile ( path ):
405 | qpFilesInInputDir . append ( file )
406 |
407 | numberOfQpFiles = len ( qpFilesInInputDir )
408 |
409 | if numberOfQpFiles == 0 :
410 | self . updateStatus . emit ( "No mapsets found in " + self . inputPath )
411 | return
412 |
413 | self . updateProgressbarMax . emit ( numberOfQpFiles )
414 |
415 | start = time . time ()
416 | count = 1
417 |
418 | for file in qpFilesInInputDir :
419 | filePath = os . path . join ( self . inputPath , file )
420 |
421 | self . updateStatus . emit ( f "( {count} / {numberOfQpFiles} ) "
422 | f "Converting {filePath} " )
423 |
424 | convertMapset ( filePath , self . outputPath , self . options )
425 | count += 1
426 | self . incrementProgressbarValue . emit ()
427 |
428 | end = time . time ()
429 | timeElapsed = round ( end - start , 2 )
430 |
431 | self . updateStatus . emit (
432 | f "Finished converting all mapsets,"
433 | f "total time elapsed: {timeElapsed} seconds"
434 | )
435 |
436 |
437 |
438 |
439 |
440 |
443 |
Opens output folder in explorer
444 |
445 |
446 |
absoluteOutputPath = os . path . realpath ( self . outputPath )
447 | webbrowser . open ( "file:///" + absoluteOutputPath )
448 |
449 | return
450 |
451 |
452 |
453 |
454 |
455 |
458 |
Custom QApplication class for the sole purpose of applying the Fusion style
459 |
460 |
461 |
class IceApp ( QApplication ):
462 |
463 |
464 |
465 |
476 |
477 |
478 |
484 |
485 |
def __init__ ( self ):
486 | super () . __init__ ( sys . argv )
487 | self . setStyle ( "Fusion" )
488 |
489 |
490 |
491 |
492 |
498 |
499 |
def main ():
500 | app = IceApp ()
501 | iceApp = IceMainWindow ()
502 | iceApp . show ()
503 | sys . exit ( app . exec_ ())
504 |
505 |
506 | if __name__ == '__main__' :
507 | main ()
508 |
509 |
510 |
511 |
512 |
513 |
514 |
515 |
--------------------------------------------------------------------------------
/docs/qua2osu.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
qua2osu.py
6 |
7 |
8 |
9 |
10 |
11 |
14 |
15 |
16 |
17 |
20 |
Command-line tool for map conversion
21 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
import argparse # parsing command line arguments
36 | import os # for paths and directories
37 | import re
38 | import sys # used only for sys.exit()
39 | import time # to measure execution time
40 | import webbrowser # to open the explorer cross-platform
41 | import zipfile # to handle .zip files (.qua and .osz)
42 |
43 | from reamber.algorithms.convert import QuaToOsu
44 | from reamber.quaver import QuaMap
45 |
46 |
47 |
48 |
49 |
55 |
56 |
SAMPLESETS = [
57 | "Soft" ,
58 | "Normal" ,
59 | "Drum"
60 | ]
61 |
62 |
63 |
64 |
75 |
76 |
77 |
78 |
81 |
Creates an argument parser with all of the arguments already added
82 |
83 |
84 |
def initArgParser () -> argparse . ArgumentParser :
85 |
86 |
87 |
88 |
89 |
95 |
96 |
argParser = argparse . ArgumentParser ( "Converts .qp files to .osz files" )
97 |
98 |
99 |
100 |
101 |
107 |
108 |
def qpOrDirPath ( inputPath ):
109 | if ( inputPath . endswith ( ".qp" ) and os . path . isFile ( inputPath )) or os . path . isdir ( inputPath ):
110 | return inputPath
111 | else :
112 | raise argparse . ArgumentTypeError ( "Path is not a directory or not a .qp file" )
113 |
114 | argParser . add_argument (
115 | "input" ,
116 | help = "Paths of directories containing .qp files, or direct paths to .qp files. Both are possible." ,
117 | nargs = "*" ,
118 | type = qpOrDirPath
119 | )
120 |
121 |
122 |
123 |
124 |
130 |
131 |
def directory ( path ):
132 | if os . path . isdir ( path ):
133 | return path
134 | raise argparse . ArgumentTypeError ( "Not a valid path" )
135 |
136 | argParser . add_argument (
137 | "-o" ,
138 | "--output" ,
139 | required = False ,
140 | help = "Path of the output folder, defaults to ./output" ,
141 | default = "./output" ,
142 | type = directory
143 | )
144 |
145 |
146 |
147 |
148 |
154 |
155 |
def diffValue ( x ):
156 | x = float ( x )
157 | if x >= 0 and x <= 10 :
158 | return x
159 | else :
160 | raise argparse . ArgumentTypeError ( "Value must be larger between 0 and 10" )
161 |
162 | argParser . add_argument (
163 | "-od" ,
164 | "--overall-difficulty" ,
165 | required = False ,
166 | help = "Overall difficulty as an integer between 0 and 10, defaults to 8" ,
167 | default = 8 ,
168 | type = diffValue
169 | )
170 |
171 | argParser . add_argument (
172 | "-hp" ,
173 | "--hp-drain" ,
174 | required = False ,
175 | help = "HP drain as an integer between 0 and 10, defaults to 8" ,
176 | default = 8 ,
177 | type = diffValue
178 | )
179 |
180 |
181 |
182 |
183 |
189 |
190 |
def hsVolume ( n ):
191 | n = int ( n )
192 | if n >= 0 and n <= 100 :
193 | return n
194 | else :
195 | raise argparse . ArgumentTypeError ( "Value must be between 0 and 100" )
196 |
197 | argParser . add_argument (
198 | "-hv" ,
199 | "--hitsound-volume" ,
200 | required = False ,
201 | help = "Hitsound volume as an integer between 0 and 100, defaults to 20" ,
202 | default = 20 ,
203 | type = hsVolume
204 | )
205 |
206 | argParser . add_argument (
207 | "-hs" ,
208 | "--sampleset" ,
209 | required = False ,
210 | help = "Hitsound sample set as either 'Soft', 'Normal' or 'Drum', defaults to Soft" ,
211 | default = "Soft" ,
212 | type = str ,
213 | choices = SAMPLESETS
214 | )
215 |
216 | argParser . add_argument (
217 | "-c" ,
218 | "--creator" ,
219 | required = False ,
220 | help = "Sets a different mapper name for all difficulties, in case Quaver username and osu! username are different" ,
221 | type = str
222 | )
223 |
224 | argParser . add_argument (
225 | "-p" ,
226 | "--preserve-folder-structure" ,
227 | required = False ,
228 | help = "Outputs in original directory structure if specified" ,
229 | action = "store_true"
230 | )
231 |
232 | argParser . add_argument (
233 | "-r" ,
234 | "--recursive-search" ,
235 | required = False ,
236 | help = "Looks for .qp in all subdirectories of given directories if specified" ,
237 | action = "store_true"
238 | )
239 |
240 | return argParser
241 |
242 |
243 |
244 |
245 |
251 |
252 |
def searchForQpFiles ( directory : str , qpList : list , recursive : bool ) -> list :
253 | for path in os . listdir ( directory ):
254 | fullRelativePath = os . path . join ( directory , path )
255 | if os . path . isfile ( fullRelativePath ) and fullRelativePath . endswith ( ".qp" ):
256 | qpList . append ( os . path . normpath ( fullRelativePath ))
257 | elif os . path . isdir ( fullRelativePath ) and recursive :
258 | searchForQpFiles ( fullRelativePath , qpList , recursive )
259 |
260 |
261 |
262 |
263 |
264 |
267 |
Converts a whole .qp mapset to a .osz mapset
268 |
269 |
270 |
def convertQp ( path : str , outputFolder : str , options ) -> None :
271 |
272 |
273 |
274 |
275 |
276 |
279 |
Moves all files to a new directory and converts all .qua files to .osu files
280 |
Options parameter is built up as following:
281 |
options = {
282 | "od": int,
283 | "hp": int,
284 | "hitSoundVolume": int,
285 | "sampleSet": ["Soft","Normal","Drum"]
286 | }
287 |
288 |
289 |
292 |
293 |
294 |
295 |
296 |
299 |
Prefixing with “q_” to prevent osu from showing the wrong preview
300 | backgrounds, because it takes the folder number to
301 | choose the background for whatever reason
302 |
303 |
304 |
folderName = "q_" + os . path . basename ( path ) . replace ( ".qp" , "" )
305 | outputPath = os . path . join ( outputFolder , folderName )
306 |
307 |
308 |
309 |
310 |
311 |
314 |
Opens the .qp (.zip) mapset file and extracts it into a folder in the same directory
315 |
316 |
317 |
with zipfile . ZipFile ( path , "r" ) as oldDir :
318 | oldDir . extractall ( outputPath )
319 |
320 |
321 |
322 |
323 |
324 |
327 |
Converts each .qua difficulty file
328 |
329 |
330 |
for file in os . listdir ( outputPath ):
331 | filePath = os . path . join ( outputPath , file )
332 |
333 |
334 |
335 |
336 |
337 |
340 |
Replaces each .qua file with the converted .osu file, uses Evening’s reamber package
341 |
342 |
343 |
if file . endswith ( ".qua" ):
344 | qua = QuaMap . readFile ( filePath )
345 | convertedOsu = QuaToOsu . convert ( qua )
346 |
347 | if options [ "od" ]:
348 | convertedOsu . overallDifficulty = options [ "od" ]
349 | if options [ "hp" ]:
350 | convertedOsu . hpDrainRate = options [ "hp" ]
351 | if options [ "creator" ]:
352 | convertedOsu . creator = options [ "creator" ]
353 |
354 | if options [ "hitSoundVolume" ] or options [ "sampleset" ]:
355 | for list in [ convertedOsu . bpms . data (), convertedOsu . svs . data ()]:
356 | for element in list :
357 | if options [ "hitSoundVolume" ]:
358 | element . volume = options [ "hitSoundVolume" ]
359 | if options [ "sampleSet" ]:
360 | element . sampleSet = SAMPLESETS . index ( options [ "sampleSet" ])
361 |
362 | newFileName = re . sub ( r "\.qua$" , ".osu" , filePath , 1 , re . MULTILINE )
363 | convertedOsu . writeFile ( newFileName )
364 | os . remove ( filePath )
365 |
366 |
367 |
368 |
369 |
370 |
373 |
Creates a new .osz (.zip) mapset file
374 |
375 |
376 |
with zipfile . ZipFile ( outputPath + ".osz" , "w" ) as newDir :
377 | for root , dirs , files in os . walk ( outputPath ):
378 | for file in files :
379 | newDir . write ( os . path . join ( root , file ), file )
380 |
381 |
382 |
383 |
384 |
385 |
388 |
Delete all files in output dir
389 |
390 |
391 |
for root , dirs , files in os . walk ( outputPath , topdown = False ):
392 | for name in files :
393 | os . remove ( os . path . join ( root , name ))
394 | for name in dirs :
395 | os . rmdir ( os . path . join ( root , name ))
396 |
397 | os . rmdir ( outputPath )
398 |
399 |
400 |
401 |
412 |
413 |
414 |
415 |
418 |
Runs the map file conversions
419 |
420 |
423 |
424 |
425 |
426 |
427 |
430 |
Run py qua2osu.py --help
for help with command line arguments
431 |
432 |
433 |
argParser = initArgParser ()
434 | args = vars ( argParser . parse_args ())
435 |
436 |
437 |
438 |
439 |
440 |
443 |
Run help command if no params
444 |
445 |
446 |
if len ( args [ "input" ]) == 0 :
447 | argParser . parse_args ([ "-h" ])
448 | sys . exit ( 1 )
449 |
450 | print ( args )
451 |
452 |
453 |
454 |
455 |
456 |
459 |
Filters for all files that end with .qp and puts the
460 | complete path of the files into an array
461 |
462 |
463 |
qpFilesInInputDir = []
464 |
465 | for path in args [ "input" ]:
466 | if os . path . isfile ( path ) and path . endswith ( ".qp" ):
467 | qpFilesInInputDir . append ( path )
468 | elif os . path . isdir ( path ):
469 | searchForQpFiles ( path , qpFilesInInputDir , args [ "recursive_search" ])
470 |
471 | print ( qpFilesInInputDir )
472 |
473 | if len ( qpFilesInInputDir ) == 0 :
474 | print ( "No mapsets found in given paths" )
475 | sys . exit ( 1 )
476 |
477 |
478 |
479 |
480 |
481 |
484 |
Assigns the arguments to an options object to pass to
485 | the convertQp()
function
486 |
487 |
488 |
options = {
489 | "od" : args [ "overall_difficulty" ],
490 | "hp" : args [ "hp_drain" ],
491 | "hitSoundVolume" : args [ "hitsound_volume" ],
492 | "sampleSet" : args [ "sampleset" ],
493 | "creator" : args [ "creator" ]
494 | }
495 |
496 |
497 |
498 |
499 |
500 |
503 |
Starts the timer for the total execution time
504 |
505 |
508 |
509 |
510 |
511 |
512 |
515 |
Run the conversion for each .qp file
516 |
517 |
518 |
for file in qpFilesInInputDir :
519 | basePath = os . path . dirname ( file ) if args [ "preserve_folder_structure" ] else ""
520 |
521 | outputPath = os . path . join ( args [ "output" ], basePath )
522 | if not os . path . exists ( outputPath ):
523 | os . mkdir ( outputPath )
524 |
525 | print ( f "Converting {file} " )
526 | convertQp ( file , outputPath , options )
527 |
528 |
529 |
530 |
531 |
532 |
535 |
Stops the timer for the total execution time
536 |
537 |
538 |
end = time . time ()
539 | timeElapsed = round ( end - start , 2 )
540 |
541 | print ( f "Finished converting all mapsets, total time elapsed: {timeElapsed} seconds" )
542 |
543 |
544 |
545 |
546 |
547 |
550 |
Opens output folder in explorer
551 |
552 |
553 |
absoluteOutputPath = os . path . realpath ( args [ "output" ])
554 | webbrowser . open ( "file:///" + absoluteOutputPath )
555 |
556 |
557 | if __name__ == '__main__' :
558 | main ()
559 | sys . exit ( 0 )
560 |
561 |
562 |
563 |
564 |
565 |
566 |
567 |
--------------------------------------------------------------------------------
/gui/gui.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Form implementation generated from reading ui file 'gui/gui.ui'
4 | #
5 | # Created by: PyQt5 UI code generator 5.13.2
6 | #
7 | # WARNING! All changes made in this file will be lost!
8 |
9 |
10 | from PyQt5 import QtCore, QtGui, QtWidgets
11 |
12 |
13 | class Ui_MainWindow(object):
14 | def setupUi(self, MainWindow):
15 | MainWindow.setObjectName("MainWindow")
16 | MainWindow.setEnabled(True)
17 | MainWindow.resize(587, 193)
18 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
19 | sizePolicy.setHorizontalStretch(0)
20 | sizePolicy.setVerticalStretch(0)
21 | sizePolicy.setHeightForWidth(MainWindow.sizePolicy().hasHeightForWidth())
22 | MainWindow.setSizePolicy(sizePolicy)
23 | MainWindow.setMinimumSize(QtCore.QSize(0, 0))
24 | MainWindow.setMaximumSize(QtCore.QSize(99999, 99999))
25 | MainWindow.setFocusPolicy(QtCore.Qt.ClickFocus)
26 | MainWindow.setContextMenuPolicy(QtCore.Qt.NoContextMenu)
27 | MainWindow.setAcceptDrops(False)
28 | MainWindow.setTabShape(QtWidgets.QTabWidget.Rounded)
29 | self.centralwidget = QtWidgets.QWidget(MainWindow)
30 | self.centralwidget.setObjectName("centralwidget")
31 | self.gridLayout_2 = QtWidgets.QGridLayout(self.centralwidget)
32 | self.gridLayout_2.setObjectName("gridLayout_2")
33 | self.valuesGroupBox = QtWidgets.QGroupBox(self.centralwidget)
34 | self.valuesGroupBox.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter)
35 | self.valuesGroupBox.setObjectName("valuesGroupBox")
36 | self.verticalLayout = QtWidgets.QVBoxLayout(self.valuesGroupBox)
37 | self.verticalLayout.setObjectName("verticalLayout")
38 | self.valuesFormLayout = QtWidgets.QFormLayout()
39 | self.valuesFormLayout.setLabelAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
40 | self.valuesFormLayout.setFormAlignment(QtCore.Qt.AlignCenter)
41 | self.valuesFormLayout.setObjectName("valuesFormLayout")
42 | self.label = QtWidgets.QLabel(self.valuesGroupBox)
43 | self.label.setObjectName("label")
44 | self.valuesFormLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.label)
45 | self.odDoubleSpinBox = QtWidgets.QDoubleSpinBox(self.valuesGroupBox)
46 | self.odDoubleSpinBox.setDecimals(1)
47 | self.odDoubleSpinBox.setMaximum(10.0)
48 | self.odDoubleSpinBox.setSingleStep(0.5)
49 | self.odDoubleSpinBox.setProperty("value", 8.0)
50 | self.odDoubleSpinBox.setObjectName("odDoubleSpinBox")
51 | self.valuesFormLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.odDoubleSpinBox)
52 | self.label_2 = QtWidgets.QLabel(self.valuesGroupBox)
53 | self.label_2.setObjectName("label_2")
54 | self.valuesFormLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.label_2)
55 | self.label_3 = QtWidgets.QLabel(self.valuesGroupBox)
56 | self.label_3.setObjectName("label_3")
57 | self.valuesFormLayout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.label_3)
58 | self.hsVolumeSpinBox = QtWidgets.QSpinBox(self.valuesGroupBox)
59 | self.hsVolumeSpinBox.setMaximum(100)
60 | self.hsVolumeSpinBox.setSingleStep(5)
61 | self.hsVolumeSpinBox.setProperty("value", 20)
62 | self.hsVolumeSpinBox.setObjectName("hsVolumeSpinBox")
63 | self.valuesFormLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.hsVolumeSpinBox)
64 | self.hpDoubleSpinBox = QtWidgets.QDoubleSpinBox(self.valuesGroupBox)
65 | self.hpDoubleSpinBox.setDecimals(1)
66 | self.hpDoubleSpinBox.setMaximum(10.0)
67 | self.hpDoubleSpinBox.setSingleStep(0.5)
68 | self.hpDoubleSpinBox.setProperty("value", 8.0)
69 | self.hpDoubleSpinBox.setObjectName("hpDoubleSpinBox")
70 | self.valuesFormLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.hpDoubleSpinBox)
71 | self.verticalLayout.addLayout(self.valuesFormLayout)
72 | self.gridLayout_2.addWidget(self.valuesGroupBox, 0, 1, 1, 1)
73 | self.sampleSetsGroupBox = QtWidgets.QGroupBox(self.centralwidget)
74 | self.sampleSetsGroupBox.setObjectName("sampleSetsGroupBox")
75 | self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.sampleSetsGroupBox)
76 | self.verticalLayout_4.setObjectName("verticalLayout_4")
77 | self.sampleSetVLayout = QtWidgets.QVBoxLayout()
78 | self.sampleSetVLayout.setObjectName("sampleSetVLayout")
79 | self.sampleSetNormalRadioButton = QtWidgets.QRadioButton(self.sampleSetsGroupBox)
80 | self.sampleSetNormalRadioButton.setChecked(False)
81 | self.sampleSetNormalRadioButton.setObjectName("sampleSetNormalRadioButton")
82 | self.sampleSetVLayout.addWidget(self.sampleSetNormalRadioButton)
83 | self.sampleSetSoftRadioButton = QtWidgets.QRadioButton(self.sampleSetsGroupBox)
84 | self.sampleSetSoftRadioButton.setChecked(True)
85 | self.sampleSetSoftRadioButton.setObjectName("sampleSetSoftRadioButton")
86 | self.sampleSetVLayout.addWidget(self.sampleSetSoftRadioButton)
87 | self.sampleSetDrumRadioButton = QtWidgets.QRadioButton(self.sampleSetsGroupBox)
88 | self.sampleSetDrumRadioButton.setObjectName("sampleSetDrumRadioButton")
89 | self.sampleSetVLayout.addWidget(self.sampleSetDrumRadioButton)
90 | self.verticalLayout_4.addLayout(self.sampleSetVLayout)
91 | self.gridLayout_2.addWidget(self.sampleSetsGroupBox, 0, 2, 1, 1)
92 | self.pathsGroupBox = QtWidgets.QGroupBox(self.centralwidget)
93 | self.pathsGroupBox.setObjectName("pathsGroupBox")
94 | self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.pathsGroupBox)
95 | self.verticalLayout_2.setObjectName("verticalLayout_2")
96 | self.pathsGridLayout = QtWidgets.QGridLayout()
97 | self.pathsGridLayout.setObjectName("pathsGridLayout")
98 | self.outputLineEdit = QtWidgets.QLineEdit(self.pathsGroupBox)
99 | self.outputLineEdit.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates))
100 | self.outputLineEdit.setObjectName("outputLineEdit")
101 | self.pathsGridLayout.addWidget(self.outputLineEdit, 1, 1, 1, 1)
102 | self.outputToolButton = QtWidgets.QToolButton(self.pathsGroupBox)
103 | self.outputToolButton.setObjectName("outputToolButton")
104 | self.pathsGridLayout.addWidget(self.outputToolButton, 1, 2, 1, 1)
105 | self.inputToolButton = QtWidgets.QToolButton(self.pathsGroupBox)
106 | self.inputToolButton.setPopupMode(QtWidgets.QToolButton.DelayedPopup)
107 | self.inputToolButton.setObjectName("inputToolButton")
108 | self.pathsGridLayout.addWidget(self.inputToolButton, 0, 2, 1, 1)
109 | self.inputLabel = QtWidgets.QLabel(self.pathsGroupBox)
110 | self.inputLabel.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
111 | self.inputLabel.setObjectName("inputLabel")
112 | self.pathsGridLayout.addWidget(self.inputLabel, 0, 0, 1, 1)
113 | self.outputLabel = QtWidgets.QLabel(self.pathsGroupBox)
114 | self.outputLabel.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
115 | self.outputLabel.setObjectName("outputLabel")
116 | self.pathsGridLayout.addWidget(self.outputLabel, 1, 0, 1, 1)
117 | self.inputLineEdit = QtWidgets.QLineEdit(self.pathsGroupBox)
118 | self.inputLineEdit.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates))
119 | self.inputLineEdit.setObjectName("inputLineEdit")
120 | self.pathsGridLayout.addWidget(self.inputLineEdit, 0, 1, 1, 1)
121 | self.verticalLayout_2.addLayout(self.pathsGridLayout)
122 | self.gridLayout_2.addWidget(self.pathsGroupBox, 0, 0, 1, 1)
123 | self.progressBar = QtWidgets.QProgressBar(self.centralwidget)
124 | self.progressBar.setProperty("value", 0)
125 | self.progressBar.setTextVisible(False)
126 | self.progressBar.setOrientation(QtCore.Qt.Horizontal)
127 | self.progressBar.setInvertedAppearance(False)
128 | self.progressBar.setObjectName("progressBar")
129 | self.gridLayout_2.addWidget(self.progressBar, 1, 0, 1, 2)
130 | self.convertPushButton = QtWidgets.QPushButton(self.centralwidget)
131 | self.convertPushButton.setFlat(False)
132 | self.convertPushButton.setObjectName("convertPushButton")
133 | self.gridLayout_2.addWidget(self.convertPushButton, 1, 2, 1, 1)
134 | self.statusLabel = QtWidgets.QLabel(self.centralwidget)
135 | self.statusLabel.setText("")
136 | self.statusLabel.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop)
137 | self.statusLabel.setWordWrap(True)
138 | self.statusLabel.setObjectName("statusLabel")
139 | self.gridLayout_2.addWidget(self.statusLabel, 2, 0, 1, 3)
140 | MainWindow.setCentralWidget(self.centralwidget)
141 | self.statusbar = QtWidgets.QStatusBar(MainWindow)
142 | self.statusbar.setObjectName("statusbar")
143 | MainWindow.setStatusBar(self.statusbar)
144 | self.label.setBuddy(self.odDoubleSpinBox)
145 | self.label_2.setBuddy(self.hpDoubleSpinBox)
146 | self.label_3.setBuddy(self.hsVolumeSpinBox)
147 | self.inputLabel.setBuddy(self.inputLineEdit)
148 | self.outputLabel.setBuddy(self.outputLineEdit)
149 |
150 | self.retranslateUi(MainWindow)
151 | QtCore.QMetaObject.connectSlotsByName(MainWindow)
152 |
153 | def retranslateUi(self, MainWindow):
154 | _translate = QtCore.QCoreApplication.translate
155 | MainWindow.setWindowTitle(_translate("MainWindow", "qua2osu"))
156 | self.valuesGroupBox.setTitle(_translate("MainWindow", "Values"))
157 | self.label.setText(_translate("MainWindow", "OD"))
158 | self.label_2.setText(_translate("MainWindow", "HP"))
159 | self.label_3.setText(_translate("MainWindow", "Hitsound Volume"))
160 | self.hsVolumeSpinBox.setSuffix(_translate("MainWindow", "%"))
161 | self.sampleSetsGroupBox.setTitle(_translate("MainWindow", "Sample Set"))
162 | self.sampleSetNormalRadioButton.setText(_translate("MainWindow", "Normal"))
163 | self.sampleSetSoftRadioButton.setText(_translate("MainWindow", "Soft"))
164 | self.sampleSetDrumRadioButton.setText(_translate("MainWindow", "Drum"))
165 | self.pathsGroupBox.setTitle(_translate("MainWindow", "Paths"))
166 | self.outputLineEdit.setPlaceholderText(_translate("MainWindow", "qua2osu\\output"))
167 | self.outputToolButton.setToolTip(_translate("MainWindow", "
Open a file dialog to choose where your resulting .osz files should be put
"))
168 | self.outputToolButton.setText(_translate("MainWindow", "..."))
169 | self.inputToolButton.setToolTip(_translate("MainWindow", "
Open a file dialog to select a folder of .qp files
"))
170 | self.inputToolButton.setText(_translate("MainWindow", "..."))
171 | self.inputLabel.setText(_translate("MainWindow", "Input Folder"))
172 | self.outputLabel.setText(_translate("MainWindow", "Output Folder"))
173 | self.inputLineEdit.setPlaceholderText(_translate("MainWindow", "qua2osu\\input"))
174 | self.convertPushButton.setText(_translate("MainWindow", "Convert"))
175 |
176 |
177 | if __name__ == "__main__":
178 | import sys
179 | app = QtWidgets.QApplication(sys.argv)
180 | MainWindow = QtWidgets.QMainWindow()
181 | ui = Ui_MainWindow()
182 | ui.setupUi(MainWindow)
183 | MainWindow.show()
184 | sys.exit(app.exec_())
185 |
--------------------------------------------------------------------------------
/gui/gui.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | MainWindow
4 |
5 |
6 | true
7 |
8 |
9 |
10 | 0
11 | 0
12 | 587
13 | 193
14 |
15 |
16 |
17 |
18 | 0
19 | 0
20 |
21 |
22 |
23 |
24 | 0
25 | 0
26 |
27 |
28 |
29 |
30 | 99999
31 | 99999
32 |
33 |
34 |
35 | Qt::ClickFocus
36 |
37 |
38 | Qt::NoContextMenu
39 |
40 |
41 | false
42 |
43 |
44 | qua2osu
45 |
46 |
47 | QTabWidget::Rounded
48 |
49 |
50 |
51 | -
52 |
53 |
54 | Values
55 |
56 |
57 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
58 |
59 |
60 | -
61 |
62 |
63 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
64 |
65 |
66 | Qt::AlignCenter
67 |
68 | -
69 |
70 |
71 | OD
72 |
73 |
74 | odDoubleSpinBox
75 |
76 |
77 |
78 | -
79 |
80 |
81 | 1
82 |
83 |
84 | 10.000000000000000
85 |
86 |
87 | 0.500000000000000
88 |
89 |
90 | 8.000000000000000
91 |
92 |
93 |
94 | -
95 |
96 |
97 | HP
98 |
99 |
100 | hpDoubleSpinBox
101 |
102 |
103 |
104 | -
105 |
106 |
107 | Hitsound Volume
108 |
109 |
110 | hsVolumeSpinBox
111 |
112 |
113 |
114 | -
115 |
116 |
117 | %
118 |
119 |
120 | 100
121 |
122 |
123 | 5
124 |
125 |
126 | 20
127 |
128 |
129 |
130 | -
131 |
132 |
133 | 1
134 |
135 |
136 | 10.000000000000000
137 |
138 |
139 | 0.500000000000000
140 |
141 |
142 | 8.000000000000000
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 | -
152 |
153 |
154 | Sample Set
155 |
156 |
157 | -
158 |
159 | -
160 |
161 |
162 | Normal
163 |
164 |
165 | false
166 |
167 |
168 |
169 | -
170 |
171 |
172 | Soft
173 |
174 |
175 | true
176 |
177 |
178 |
179 | -
180 |
181 |
182 | Drum
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 | -
192 |
193 |
194 | Paths
195 |
196 |
197 | -
198 |
199 | -
200 |
201 |
202 |
203 |
204 |
205 | qua2osu\output
206 |
207 |
208 |
209 | -
210 |
211 |
212 | <html><head/><body><p>Open a file dialog to choose where your resulting .osz files should be put</p></body></html>
213 |
214 |
215 | ...
216 |
217 |
218 |
219 | -
220 |
221 |
222 | <html><head/><body><p>Open a file dialog to select a folder of .qp files</p></body></html>
223 |
224 |
225 | ...
226 |
227 |
228 | QToolButton::DelayedPopup
229 |
230 |
231 |
232 | -
233 |
234 |
235 | Input Folder
236 |
237 |
238 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
239 |
240 |
241 | inputLineEdit
242 |
243 |
244 |
245 | -
246 |
247 |
248 | Output Folder
249 |
250 |
251 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
252 |
253 |
254 | outputLineEdit
255 |
256 |
257 |
258 | -
259 |
260 |
261 |
262 |
263 |
264 | qua2osu\input
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 | -
274 |
275 |
276 | 0
277 |
278 |
279 | false
280 |
281 |
282 | Qt::Horizontal
283 |
284 |
285 | false
286 |
287 |
288 |
289 | -
290 |
291 |
292 | Convert
293 |
294 |
295 | false
296 |
297 |
298 |
299 | -
300 |
301 |
302 |
303 |
304 |
305 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop
306 |
307 |
308 | true
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
--------------------------------------------------------------------------------
/qua2osu-gui.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | import time
4 | import webbrowser # to open the explorer cross-platform
5 |
6 | from PyQt5.QtCore import *
7 | from PyQt5.QtGui import *
8 | from PyQt5.QtWidgets import *
9 |
10 | from conversion import convertMapset
11 |
12 | # gui.py is autogenerated from gui.ui (qt designer)
13 |
14 | # use "pyuic5 -x gui/gui.ui -o gui/gui.py" after editing gui.ui with qt designer
15 | from gui.gui import Ui_MainWindow
16 |
17 |
18 | class IceMainWindow(QMainWindow, Ui_MainWindow):
19 | """Custom window class with all of the GUI functionality
20 |
21 | All of the objects themselves are managed by QT designer in the gui.ui (gui.py) file
22 | """
23 |
24 | def __init__(self):
25 | QMainWindow.__init__(self)
26 | self.setupUi(self)
27 | self.inputToolButton.clicked.connect(self.openInputDirectoryDialog)
28 | self.outputToolButton.clicked.connect(self.openOutputDirectoryDialog)
29 | self.convertPushButton.clicked.connect(self.convertOnClick)
30 |
31 | self.updateStatus("Finished setup")
32 |
33 | def openInputDirectoryDialog(self):
34 | """Opens the input directory dialog and sets the input path in the GUI"""
35 |
36 | path = str(QFileDialog.getExistingDirectory(
37 | self, "Select Input Folder"))
38 | self.inputLineEdit.setText(path)
39 | self.updateStatus("Set input path to " + path)
40 |
41 | def openOutputDirectoryDialog(self):
42 | """Opens the output directory dialog and sets the input path in the GUI"""
43 |
44 | path = str(QFileDialog.getExistingDirectory(
45 | self, "Select Output Folder"))
46 | self.outputLineEdit.setText(path)
47 | self.updateStatus("Set output path to " + path)
48 |
49 | def updateStatus(self, status):
50 | """Wrapper for updating the status label, always shows the two last lines"""
51 |
52 | currentText = self.statusLabel.text()
53 | lastLine = currentText.split("\n")[-1]
54 | self.statusLabel.setText(lastLine + "\n" + status)
55 |
56 | def updateProgressBarMax(self, maximum):
57 | """Wrapper for updating the progress bar maximum"""
58 |
59 | self.progressBar.setMaximum(maximum)
60 |
61 | def incrementProgressBarValue(self):
62 | """Increments the progress bar by one unit total, adds a smooth animation"""
63 |
64 | self.progressBar.setValue(self.progressBar.value() + 1)
65 |
66 | def initiateConverterThread(self, inputPath, outputPath, options):
67 | """Sets up the converter thread"""
68 |
69 | converterThread = ConverterThread(inputPath, outputPath, options)
70 | converterThread.updateStatus.connect(self.updateStatus)
71 | converterThread.updateProgressbarMax.connect(
72 | self.updateProgressBarMax)
73 | converterThread.incrementProgressbarValue.connect(
74 | self.incrementProgressBarValue)
75 |
76 | return converterThread
77 |
78 | def convertOnClick(self):
79 | """Starts the map conversion of the folder"""
80 |
81 | inputPath = self.inputLineEdit.text()
82 | outputPath = self.outputLineEdit.text()
83 |
84 | if inputPath == "" or outputPath == "":
85 | self.updateStatus("Empty paths detected, using defaults")
86 |
87 | if inputPath == "":
88 | inputPath = "input"
89 |
90 | if outputPath == "":
91 | outputPath = "output"
92 |
93 | selectedOd = self.odDoubleSpinBox.value()
94 | selectedHp = self.hpDoubleSpinBox.value()
95 | selectedHitSoundVolume = self.hsVolumeSpinBox.value()
96 |
97 | # please tell me if there's an easier way to lookup
98 | # which radio button is checked because this is horrible
99 | selectedSampleSet = ""
100 | sampleSetButtons = [
101 | self.sampleSetNormalRadioButton,
102 | self.sampleSetSoftRadioButton,
103 | self.sampleSetDrumRadioButton
104 | ]
105 |
106 | for button in sampleSetButtons:
107 | if button.isChecked():
108 | selectedSampleSet = button.text()
109 | break
110 |
111 | options = {
112 | "od": selectedOd,
113 | "hp": selectedHp,
114 | "hitSoundVolume": selectedHitSoundVolume,
115 | "sampleSet": selectedSampleSet
116 | }
117 |
118 | self.progressBar.setValue(0)
119 |
120 | self.converterThread = self.initiateConverterThread(
121 | inputPath, outputPath, options)
122 | self.converterThread.start()
123 |
124 |
125 | class ConverterThread(QThread):
126 | """Using a different thread for the map conversion
127 |
128 | Otherwise the UI would freeze and become unresponsive during the conversion
129 | """
130 |
131 | updateStatus = pyqtSignal(str)
132 | incrementProgressbarValue = pyqtSignal()
133 | updateProgressbarMax = pyqtSignal(int)
134 |
135 | def __init__(self, inputPath, outputPath, options):
136 | QThread.__init__(self)
137 | self.inputPath = inputPath
138 | self.outputPath = outputPath
139 | self.options = options
140 |
141 | def __del__(self):
142 | self.wait()
143 |
144 | def run(self):
145 | qpFilesInInputDir = []
146 |
147 | for file in os.listdir(self.inputPath):
148 | path = os.path.join(self.inputPath, file)
149 | if file.endswith('.qp') and os.path.isfile(path):
150 | qpFilesInInputDir.append(file)
151 |
152 | numberOfQpFiles = len(qpFilesInInputDir)
153 |
154 | if numberOfQpFiles == 0:
155 | self.updateStatus.emit("No mapsets found in " + self.inputPath)
156 | return
157 |
158 | self.updateProgressbarMax.emit(numberOfQpFiles)
159 |
160 | start = time.time()
161 | count = 1
162 |
163 | for file in qpFilesInInputDir:
164 | filePath = os.path.join(self.inputPath, file)
165 |
166 | self.updateStatus.emit(f"({count}/{numberOfQpFiles}) "
167 | f"Converting {filePath}")
168 |
169 | convertMapset(filePath, self.outputPath, self.options)
170 | count += 1
171 | self.incrementProgressbarValue.emit()
172 |
173 | end = time.time()
174 | timeElapsed = round(end - start, 2)
175 |
176 | self.updateStatus.emit(
177 | f"Finished converting all mapsets,"
178 | f"total time elapsed: {timeElapsed} seconds"
179 | )
180 |
181 | # Opens output folder in explorer
182 | absoluteOutputPath = os.path.realpath(self.outputPath)
183 | webbrowser.open("file:///" + absoluteOutputPath)
184 |
185 | return
186 |
187 |
188 | class IceApp(QApplication):
189 | """Custom QApplication class for the sole purpose of applying the Fusion style"""
190 |
191 | def __init__(self):
192 | super().__init__(sys.argv)
193 | self.setStyle("Fusion")
194 |
195 |
196 | def main():
197 | app = IceApp()
198 | iceApp = IceMainWindow()
199 | iceApp.show()
200 | sys.exit(app.exec_())
201 |
202 |
203 | if __name__ == '__main__':
204 | main()
205 |
--------------------------------------------------------------------------------
/qua2osu.py:
--------------------------------------------------------------------------------
1 | """Command-line tool for map conversion"""
2 |
3 | # ## Imports
4 |
5 | import argparse # parsing command line arguments
6 | import os # for paths and directories
7 | import re
8 | import sys # used only for sys.exit()
9 | import time # to measure execution time
10 | import webbrowser # to open the explorer cross-platform
11 | import zipfile # to handle .zip files (.qua and .osz)
12 |
13 | from reamber.algorithms.convert import QuaToOsu
14 | from reamber.quaver import QuaMap
15 |
16 | # ## Constants
17 |
18 | SAMPLESETS = [
19 | "Soft",
20 | "Normal",
21 | "Drum"
22 | ]
23 |
24 | # ## Functions
25 |
26 |
27 | def initArgParser() -> argparse.ArgumentParser:
28 | """Creates an argument parser with all of the arguments already added"""
29 |
30 | argParser = argparse.ArgumentParser("Converts .qp files to .osz files")
31 |
32 | def qpOrDirPath(inputPath):
33 | if (inputPath.endswith(".qp") and os.path.isFile(inputPath)) or os.path.isdir(inputPath):
34 | return inputPath
35 | else:
36 | raise argparse.ArgumentTypeError("Path is not a directory or not a .qp file")
37 |
38 | argParser.add_argument(
39 | "input",
40 | help="Paths of directories containing .qp files, or direct paths to .qp files. Both are possible.",
41 | nargs="*",
42 | type=qpOrDirPath
43 | )
44 |
45 | def directory(path):
46 | if os.path.isdir(path):
47 | return path
48 | raise argparse.ArgumentTypeError("Not a valid path")
49 |
50 | argParser.add_argument(
51 | "-o",
52 | "--output",
53 | required=False,
54 | help="Path of the output folder, defaults to ./output",
55 | default="./output",
56 | type=directory
57 | )
58 |
59 | def diffValue(x):
60 | x = float(x)
61 | if x >= 0 and x <= 10:
62 | return x
63 | else:
64 | raise argparse.ArgumentTypeError("Value must be larger between 0 and 10")
65 |
66 | argParser.add_argument(
67 | "-od",
68 | "--overall-difficulty",
69 | required=False,
70 | help="Overall difficulty as an integer between 0 and 10, defaults to 8",
71 | default=8,
72 | type=diffValue
73 | )
74 |
75 | argParser.add_argument(
76 | "-hp",
77 | "--hp-drain",
78 | required=False,
79 | help="HP drain as an integer between 0 and 10, defaults to 8",
80 | default=8,
81 | type=diffValue
82 | )
83 |
84 | def hsVolume(n):
85 | n = int(n)
86 | if n >= 0 and n <= 100:
87 | return n
88 | else:
89 | raise argparse.ArgumentTypeError("Value must be between 0 and 100")
90 |
91 | argParser.add_argument(
92 | "-hv",
93 | "--hitsound-volume",
94 | required=False,
95 | help="Hitsound volume as an integer between 0 and 100, defaults to 20",
96 | default=20,
97 | type=hsVolume
98 | )
99 |
100 | argParser.add_argument(
101 | "-hs",
102 | "--sampleset",
103 | required=False,
104 | help="Hitsound sample set as either 'Soft', 'Normal' or 'Drum', defaults to Soft",
105 | default="Soft",
106 | type=str,
107 | choices=SAMPLESETS
108 | )
109 |
110 | argParser.add_argument(
111 | "-c",
112 | "--creator",
113 | required=False,
114 | help="Sets a different mapper name for all difficulties, in case Quaver username and osu! username are different",
115 | type=str
116 | )
117 |
118 | argParser.add_argument(
119 | "-p",
120 | "--preserve-folder-structure",
121 | required=False,
122 | help="Outputs in original directory structure if specified",
123 | action="store_true"
124 | )
125 |
126 | argParser.add_argument(
127 | "-r",
128 | "--recursive-search",
129 | required=False,
130 | help="Looks for .qp in all subdirectories of given directories if specified",
131 | action="store_true"
132 | )
133 |
134 | return argParser
135 |
136 |
137 | def searchForQpFiles(directory: str, qpList: list, recursive: bool) -> list:
138 | for path in os.listdir(directory):
139 | fullRelativePath = os.path.join(directory, path)
140 | if os.path.isfile(fullRelativePath) and fullRelativePath.endswith(".qp"):
141 | qpList.append(os.path.normpath(fullRelativePath))
142 | elif os.path.isdir(fullRelativePath) and recursive:
143 | searchForQpFiles(fullRelativePath, qpList, recursive)
144 |
145 |
146 | def convertQp(path: str, outputFolder: str, options) -> None:
147 | """Converts a whole .qp mapset to a .osz mapset
148 |
149 | Moves all files to a new directory and converts all .qua files to .osu files
150 |
151 | Options parameter is built up as following:
152 |
153 | options = {
154 | "od": int,
155 | "hp": int,
156 | "hitSoundVolume": int,
157 | "sampleSet": ["Soft","Normal","Drum"]
158 | }
159 | """
160 |
161 | # Prefixing with "q_" to prevent osu from showing the wrong preview
162 | # backgrounds, because it takes the folder number to
163 | # choose the background for whatever reason
164 | folderName = "q_" + os.path.basename(path).replace(".qp", "")
165 | outputPath = os.path.join(outputFolder, folderName)
166 |
167 | # Opens the .qp (.zip) mapset file and extracts it into a folder in the same directory
168 | with zipfile.ZipFile(path, "r") as oldDir:
169 | oldDir.extractall(outputPath)
170 |
171 | # Converts each .qua difficulty file
172 | for file in os.listdir(outputPath):
173 | filePath = os.path.join(outputPath, file)
174 |
175 | # Replaces each .qua file with the converted .osu file, uses Evening's reamber package
176 | if file.endswith(".qua"):
177 | qua = QuaMap.readFile(filePath)
178 | convertedOsu = QuaToOsu.convert(qua)
179 |
180 | if options["od"]:
181 | convertedOsu.overallDifficulty = options["od"]
182 | if options["hp"]:
183 | convertedOsu.hpDrainRate = options["hp"]
184 | if options["creator"]:
185 | convertedOsu.creator = options["creator"]
186 |
187 | if options["hitSoundVolume"] or options["sampleset"]:
188 | for list in [convertedOsu.bpms.data(), convertedOsu.svs.data()]:
189 | for element in list:
190 | if options["hitSoundVolume"]:
191 | element.volume = options["hitSoundVolume"]
192 | if options["sampleSet"]:
193 | element.sampleSet = SAMPLESETS.index(options["sampleSet"])
194 |
195 | newFileName = re.sub(r"\.qua$", ".osu", filePath, 1, re.MULTILINE)
196 | convertedOsu.writeFile(newFileName)
197 | os.remove(filePath)
198 |
199 | # Creates a new .osz (.zip) mapset file
200 | with zipfile.ZipFile(outputPath + ".osz", "w") as newDir:
201 | for root, dirs, files in os.walk(outputPath):
202 | for file in files:
203 | newDir.write(os.path.join(root, file), file)
204 |
205 | # Delete all files in output dir
206 | for root, dirs, files in os.walk(outputPath, topdown=False):
207 | for name in files:
208 | os.remove(os.path.join(root, name))
209 | for name in dirs:
210 | os.rmdir(os.path.join(root, name))
211 |
212 | os.rmdir(outputPath)
213 |
214 | # ### Main
215 |
216 |
217 | def main():
218 | """Runs the map file conversions
219 |
220 | Run `py qua2osu.py --help` for help with command line arguments
221 | """
222 |
223 | argParser = initArgParser()
224 | args = vars(argParser.parse_args())
225 |
226 | # Run help command if no params
227 | if len(args["input"]) == 0:
228 | argParser.parse_args(["-h"])
229 | sys.exit(1)
230 |
231 | print(args)
232 |
233 | # Filters for all files that end with .qp and puts the
234 | # complete path of the files into an array
235 |
236 | qpFilesInInputDir = []
237 |
238 | for path in args["input"]:
239 | if os.path.isfile(path) and path.endswith(".qp"):
240 | qpFilesInInputDir.append(path)
241 | elif os.path.isdir(path):
242 | searchForQpFiles(path, qpFilesInInputDir, args["recursive_search"])
243 |
244 | print(qpFilesInInputDir)
245 |
246 | if len(qpFilesInInputDir) == 0:
247 | print("No mapsets found in given paths")
248 | sys.exit(1)
249 |
250 | # Assigns the arguments to an options object to pass to
251 | # the `convertQp()` function
252 | options = {
253 | "od": args["overall_difficulty"],
254 | "hp": args["hp_drain"],
255 | "hitSoundVolume": args["hitsound_volume"],
256 | "sampleSet": args["sampleset"],
257 | "creator": args["creator"]
258 | }
259 |
260 | # Starts the timer for the total execution time
261 | start = time.time()
262 |
263 | # Run the conversion for each .qp file
264 | for file in qpFilesInInputDir:
265 | basePath = os.path.dirname(file) if args["preserve_folder_structure"] else ""
266 |
267 | outputPath = os.path.join(args["output"], basePath)
268 | if not os.path.exists(outputPath):
269 | os.mkdir(outputPath)
270 |
271 | print(f"Converting {file}")
272 | convertQp(file, outputPath, options)
273 |
274 | # Stops the timer for the total execution time
275 | end = time.time()
276 | timeElapsed = round(end - start, 2)
277 |
278 | print(f"Finished converting all mapsets, total time elapsed: {timeElapsed} seconds")
279 |
280 | # Opens output folder in explorer
281 | absoluteOutputPath = os.path.realpath(args["output"])
282 | webbrowser.open("file:///" + absoluteOutputPath)
283 |
284 |
285 | if __name__ == '__main__':
286 | main()
287 | sys.exit(0)
288 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IceDynamix/qua2osu/380665b41af4ec9d41b7d4940d83766896785898/requirements.txt
--------------------------------------------------------------------------------