mirror of
https://github.com/Melon-Bread/RetroUFO
synced 2024-11-24 16:28:30 -05:00
commit
3bd1159381
29
README.md
29
README.md
@ -1,11 +1,11 @@
|
||||
# RetroUFO
|
||||
[![Platform](https://img.shields.io/badge/platform-linux%20%7C%20windows-yellow.svg)](https://www.youtube.com/watch?v=NLGoKxh8Aq4)
|
||||
[![Platform](https://img.shields.io/badge/platform-linux%20%7C%20macos%20%7C%20windows-yellow.svg)](https://www.youtube.com/watch?v=NLGoKxh8Aq4)
|
||||
[![Python Version](https://img.shields.io/pypi/pyversions/Django.svg)](https://www.python.org/downloads/) [![License.](https://img.shields.io/github/license/mashape/apistatus.svg)](https://opensource.org/licenses/MIT)
|
||||
|
||||
A ~~messy~~ Python script that grabs the latest version of every libretro core from the [build bot](https://buildbot.libretro.com/).
|
||||
|
||||
***
|
||||
### Usage
|
||||
### Usage-CLI
|
||||
|
||||
Just run the script with _Python 3_:
|
||||
|
||||
@ -21,6 +21,28 @@ If you are more of a advance user, and want to do things a bit more manually, yo
|
||||
python3 ./RetroUFO.py --help
|
||||
```
|
||||
|
||||
### Usage-GUI
|
||||
|
||||
The GUI script uses [Qt for Python](https://wiki.qt.io/Qt_for_Python) ([PySide2](https://pypi.org/project/PySide2/)). So you can make sure you have that package installed by running:
|
||||
```bash
|
||||
pip3 install --user PySide2
|
||||
```
|
||||
After that you can just run the script like so:
|
||||
```bash
|
||||
python3 ./RetroUFO_GUI.py
|
||||
```
|
||||
|
||||
You can then just click the `Grab Cores` button at the bottom and then you should be all set.
|
||||
|
||||
![](screenshots/grab_cores.gif)
|
||||
|
||||
If you would like to grab cores for a different platform or architecture you can override which supported cores it grabs.
|
||||
|
||||
![](screenshots/custom_platform.gif)
|
||||
|
||||
If you have your core directory set somewhere special you can override where the cores extract to.
|
||||
|
||||
![](screenshots/custom_location.gif)
|
||||
|
||||
***
|
||||
### TO-DO
|
||||
@ -32,5 +54,6 @@ python3 ./RetroUFO.py --help
|
||||
- ~~Auto detect platform & architecture~~
|
||||
- Download progress bar
|
||||
- ~~Keep downloaded archives~~
|
||||
- Give better console output
|
||||
- ~~Make GUI~~
|
||||
- Real error handling
|
||||
- Support for ARM detection
|
||||
|
13
RetroUFO.py
13
RetroUFO.py
@ -4,7 +4,7 @@ Grabs the latest version of every libretro core from the build bot.
|
||||
"""
|
||||
|
||||
__author__ = "Melon Bread"
|
||||
__version__ = "0.8.0"
|
||||
__version__ = "0.9.0"
|
||||
__license__ = "MIT"
|
||||
|
||||
import argparse
|
||||
@ -12,7 +12,6 @@ import os
|
||||
import platform
|
||||
import sys
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from shutil import rmtree
|
||||
from urllib.request import urlretrieve
|
||||
|
||||
@ -20,8 +19,9 @@ URL = 'https://buildbot.libretro.com/nightly'
|
||||
|
||||
# These are the default core locations with normal RetroArch installs based off of 'retroarch.default.cfg`
|
||||
CORE_LOCATION = {
|
||||
'linux': '{}/.config/retroarch/cores'.format(Path.home()),
|
||||
'windows': '{}/AppData/Roaming/RetroArch/cores'.format(Path.home())
|
||||
'linux': '{}/.config/retroarch/cores'.format(os.path.expanduser('~')),
|
||||
'apple/osx': '/Applications/RetroArch.app/Contents/Resources/cores', # macOS
|
||||
'windows': '{}/AppData/Roaming/RetroArch/cores'.format(os.path.expanduser('~'))
|
||||
}
|
||||
|
||||
|
||||
@ -29,7 +29,7 @@ def main(_args):
|
||||
""" Where the magic happens """
|
||||
|
||||
# If a platform and/or architecture is not supplied it is grabbed automatically
|
||||
platform = _args.platform if _args.platform else get_platform() # TODO: rename this var to prevent conflict
|
||||
target_platform = _args.platform if _args.platform else get_platform()
|
||||
architecture = _args.architecture if _args.architecture else get_architecture()
|
||||
location = _args.location if _args.location else CORE_LOCATION[platform]
|
||||
|
||||
@ -45,7 +45,8 @@ def get_platform():
|
||||
|
||||
if platform.system() == 'Linux':
|
||||
return 'linux'
|
||||
|
||||
elif platform.system() == 'Darwin': # macOS
|
||||
return 'apple/osx'
|
||||
elif platform.system() == 'Windows' or 'MSYS_NT' in platform.system(): # Checks for MSYS environment as well
|
||||
return 'windows'
|
||||
else:
|
||||
|
270
RetroUFO_GUI.py
Executable file
270
RetroUFO_GUI.py
Executable file
@ -0,0 +1,270 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Grabs the latest version of every libretro core from the build bot.
|
||||
"""
|
||||
|
||||
__author__ = "Melon Bread"
|
||||
__version__ = "0.9.0"
|
||||
__license__ = "MIT"
|
||||
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
import zipfile
|
||||
from shutil import rmtree
|
||||
from urllib.request import urlretrieve
|
||||
|
||||
from PySide2.QtCore import QThread, Signal
|
||||
from PySide2.QtGui import QTextCursor
|
||||
from PySide2.QtWidgets import (QApplication, QCheckBox, QComboBox, QDialog,
|
||||
QFileDialog, QLineEdit, QPushButton, QTextEdit,
|
||||
QVBoxLayout, QMessageBox)
|
||||
|
||||
URL = 'https://buildbot.libretro.com/nightly'
|
||||
|
||||
# These are the default core locations with normal RetroArch installs based off of 'retroarch.default.cfg`
|
||||
CORE_LOCATION = {
|
||||
'linux': '{}/.config/retroarch/cores'.format(os.path.expanduser('~')),
|
||||
'apple/osx': '/Applications/RetroArch.app/Contents/Resources/cores', # macOS
|
||||
'windows': '{}/AppData/Roaming/RetroArch/cores'.format(os.path.expanduser('~'))
|
||||
}
|
||||
|
||||
|
||||
class GrabThread(QThread):
|
||||
add_to_log = Signal(str)
|
||||
lock = Signal(bool)
|
||||
|
||||
def __init__(self, _platform, _architecture, _location):
|
||||
QThread.__init__(self)
|
||||
self.platform = _platform
|
||||
self.architecture = _architecture
|
||||
self.location = _location
|
||||
|
||||
def __del__(self):
|
||||
self.wait()
|
||||
|
||||
def run(self):
|
||||
self.lock.emit(True)
|
||||
self.add_to_log.emit('~Starting UFO Grabber~\n')
|
||||
self.download_cores(self.platform, self.architecture)
|
||||
self.extract_cores(self.location)
|
||||
self.lock.emit(False)
|
||||
|
||||
def create_dir(self, _name):
|
||||
if not os.path.isdir(_name):
|
||||
os.makedirs(_name)
|
||||
|
||||
def obtain_core_list(self, _platform, _architecture):
|
||||
urlretrieve(
|
||||
'{}/{}/{}/latest/.index-extended'.format(
|
||||
URL, _platform, _architecture), 'cores/index')
|
||||
self.add_to_log.emit('Obtained core index!\n')
|
||||
|
||||
def download_cores(self, _platform, _architecture):
|
||||
""" Downloads every core to the working directory """
|
||||
|
||||
cores = []
|
||||
|
||||
# Makes core directory to store archives if needed
|
||||
self.create_dir("cores")
|
||||
|
||||
# Downloads a list of all the cores available
|
||||
self.obtain_core_list(_platform, _architecture)
|
||||
|
||||
# Adds all the core's file names to a list
|
||||
core_index = open('cores/index')
|
||||
|
||||
for line in core_index:
|
||||
file_name = line.split(' ', 2)[2:]
|
||||
cores.append(file_name[0].rstrip())
|
||||
core_index.close()
|
||||
cores.sort()
|
||||
|
||||
# Downloads each core from the list
|
||||
self.add_to_log.emit('Downloading Cores\n')
|
||||
for core in cores:
|
||||
urlretrieve(
|
||||
'{}/{}/{}/latest/{}'.format(URL, _platform, _architecture,
|
||||
core), 'cores/{}'.format(core))
|
||||
self.add_to_log.emit('Downloaded {} ...'.format(core))
|
||||
|
||||
# Removes index file for easier extraction
|
||||
os.remove('cores/index')
|
||||
|
||||
def extract_cores(self, _location):
|
||||
""" Extracts each downloaded core to the RA core directory """
|
||||
self.add_to_log.emit('\nExtracting all cores to: {}\n'.format(_location))
|
||||
|
||||
for file in os.listdir('cores'):
|
||||
archive = zipfile.ZipFile('cores/{}'.format(file))
|
||||
archive.extractall(_location)
|
||||
self.add_to_log.emit('Extracted {} ...'.format(file))
|
||||
|
||||
|
||||
class Form(QDialog):
|
||||
def __init__(self, parent=None):
|
||||
super(Form, self).__init__(parent)
|
||||
self.setWindowTitle('RetroUFO')
|
||||
|
||||
# Create widgets
|
||||
self.chkboxPlatformDetect = QCheckBox('Platform Auto-Detect')
|
||||
self.chkboxPlatformDetect.setChecked(True)
|
||||
self.chkboxPlatformDetect.stateChanged.connect(self.auto_detect)
|
||||
|
||||
self.cmbboxPlatform = QComboBox()
|
||||
self.cmbboxPlatform.setEnabled(False)
|
||||
self.cmbboxPlatform.setEditable(False)
|
||||
self.cmbboxPlatform.addItem('Linux')
|
||||
self.cmbboxPlatform.addItem('macOS')
|
||||
self.cmbboxPlatform.addItem('Windows')
|
||||
|
||||
self.cmbboxArchitecture = QComboBox()
|
||||
self.cmbboxArchitecture.setEnabled(False)
|
||||
self.cmbboxArchitecture.setEditable(False)
|
||||
self.cmbboxArchitecture.addItem('x86')
|
||||
self.cmbboxArchitecture.addItem('x86_64')
|
||||
|
||||
self.chkboxLocationDetect = QCheckBox('Core Location Auto-Detect')
|
||||
self.chkboxLocationDetect.setChecked(True)
|
||||
self.chkboxLocationDetect.stateChanged.connect(self.auto_location)
|
||||
|
||||
self.leditCoreLocation = QLineEdit('')
|
||||
self.leditCoreLocation.setEnabled(False)
|
||||
|
||||
self.btnCoreLocation = QPushButton('...')
|
||||
self.btnCoreLocation.setEnabled(False)
|
||||
self.btnCoreLocation.clicked.connect(self.choose_location)
|
||||
|
||||
self.teditLog = QTextEdit()
|
||||
self.teditLog.setReadOnly(True)
|
||||
|
||||
self.tcsrLog = QTextCursor(self.teditLog.document())
|
||||
|
||||
self.chkboxKeepDownload = QCheckBox('Keep Downloaded Cores')
|
||||
self.chkboxKeepDownload.setChecked(False)
|
||||
|
||||
self.btnGrabCores = QPushButton('Grab Cores')
|
||||
self.btnGrabCores.clicked.connect(self.grab_cores)
|
||||
|
||||
# Create layout and add widgets
|
||||
self.formLayout = QVBoxLayout()
|
||||
self.formLayout.addWidget(self.chkboxPlatformDetect)
|
||||
self.formLayout.addWidget(self.cmbboxPlatform)
|
||||
self.formLayout.addWidget(self.cmbboxArchitecture)
|
||||
self.formLayout.addWidget(self.chkboxLocationDetect)
|
||||
self.formLayout.addWidget(self.leditCoreLocation)
|
||||
self.formLayout.addWidget(self.btnCoreLocation)
|
||||
self.formLayout.addWidget(self.teditLog)
|
||||
self.formLayout.addWidget(self.chkboxKeepDownload)
|
||||
self.formLayout.addWidget(self.btnGrabCores)
|
||||
|
||||
# Set dialog layout
|
||||
self.setLayout(self.formLayout)
|
||||
|
||||
def auto_detect(self):
|
||||
if self.chkboxPlatformDetect.isChecked():
|
||||
self.cmbboxPlatform.setEnabled(False)
|
||||
self.cmbboxArchitecture.setEnabled(False)
|
||||
else:
|
||||
self.cmbboxPlatform.setEnabled(True)
|
||||
self.cmbboxArchitecture.setEnabled(True)
|
||||
|
||||
def auto_location(self):
|
||||
if self.chkboxLocationDetect.isChecked():
|
||||
self.leditCoreLocation.setEnabled(False)
|
||||
self.btnCoreLocation.setEnabled(False)
|
||||
else:
|
||||
self.leditCoreLocation.setEnabled(True)
|
||||
self.btnCoreLocation.setEnabled(True)
|
||||
|
||||
def choose_location(self):
|
||||
directory = QFileDialog.getExistingDirectory(
|
||||
self, 'Choose Target Location', os.path.expanduser('~'))
|
||||
|
||||
self.leditCoreLocation.insert(directory)
|
||||
|
||||
def update_log(self, _info):
|
||||
self.teditLog.insertPlainText('{}\n'.format(_info))
|
||||
|
||||
# Auto scrolling on log UI
|
||||
self.teditLog.moveCursor(QTextCursor.End)
|
||||
|
||||
def lock_ui(self, _lock):
|
||||
# Cycle through each widget and disable it except for log UI
|
||||
widgets = (self.formLayout.itemAt(i).widget() for i in range(self.formLayout.count()))
|
||||
for widget in widgets:
|
||||
if isinstance(widget, QTextEdit):
|
||||
pass
|
||||
else:
|
||||
widget.setDisabled(_lock)
|
||||
# Have to run these to make sure only the correct things unlock after grab thread
|
||||
self.auto_detect()
|
||||
self.auto_location()
|
||||
|
||||
|
||||
def grab_cores(self):
|
||||
""" Where the magic happens """
|
||||
if not self.chkboxKeepDownload.isChecked():
|
||||
self.clean_up()
|
||||
|
||||
target_platform = self.get_platform()
|
||||
architecture = self.get_architecture()
|
||||
location = self.get_location()
|
||||
|
||||
self.grab = GrabThread(target_platform, architecture, location)
|
||||
self.grab.add_to_log.connect(self.update_log)
|
||||
self.grab.lock.connect(self.lock_ui)
|
||||
self.grab.start()
|
||||
|
||||
def get_platform(self):
|
||||
""" Gets the Platform and Architecture if not supplied """
|
||||
|
||||
if not self.chkboxPlatformDetect.isChecked():
|
||||
if self.cmbboxPlatform.currentText() == 'macOS':
|
||||
return 'apple/osx' # macOS
|
||||
else:
|
||||
return self.cmbboxPlatform.currentText().lower()
|
||||
else:
|
||||
if platform.system() == 'Linux':
|
||||
return 'linux'
|
||||
elif platform.system() == 'Darwin': # macOS
|
||||
return 'apple/osx'
|
||||
elif platform.system() == 'Windows' or 'MSYS_NT' in platform.system(): # Checks for MSYS environment as well
|
||||
return 'windows'
|
||||
else:
|
||||
msgBox = QMessageBox.warning(self, 'Error', 'Platform not found or supported!', QMessageBox.Ok)
|
||||
msgBox.exec_()
|
||||
|
||||
def get_architecture(self):
|
||||
""" Gets the Platform and Architecture if not supplied """
|
||||
|
||||
if '64' in platform.architecture()[0]:
|
||||
return 'x86_64'
|
||||
|
||||
elif '32' in platform.architecture()[0]:
|
||||
return 'x86'
|
||||
else:
|
||||
msgBox = QMessageBox.warning(self, 'Error', 'Architecture not found or supported', QMessageBox.Ok)
|
||||
msgBox.exec_()
|
||||
|
||||
def get_location(self):
|
||||
if not self.chkboxLocationDetect.isChecked():
|
||||
return self.leditCoreLocation.text()
|
||||
else:
|
||||
return CORE_LOCATION[self.get_platform()]
|
||||
|
||||
def clean_up(self):
|
||||
""" Removes all the downloaded files """
|
||||
if os.listdir('cores'):
|
||||
rmtree('cores/')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Create the Qt Application
|
||||
app = QApplication(sys.argv)
|
||||
# Create and show the form
|
||||
form = Form()
|
||||
form.setFixedWidth(438) # So all text on the log UI stays on one line
|
||||
form.show()
|
||||
# Run the main Qt loop
|
||||
sys.exit(app.exec_())
|
BIN
screenshots/custom_location.gif
Normal file
BIN
screenshots/custom_location.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 MiB |
BIN
screenshots/custom_platform.gif
Normal file
BIN
screenshots/custom_platform.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 MiB |
BIN
screenshots/grab_cores.gif
Normal file
BIN
screenshots/grab_cores.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.3 MiB |
Loading…
Reference in New Issue
Block a user