class LoadZeroPointView(QDialog): def __init__(self, zeroPointManager: ZeroPointManager): super().__init__() self.setWindowTitle("Load Zero Point") self._zeroPointManager = zeroPointManager self._listWidget = QListWidget() for zeroPoint in self._zeroPointManager.getZeroPoints(): listItem = QListWidgetItem( f"{zeroPoint.name} : {zeroPoint.offset}") self._listWidget.addItem(listItem) buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) buttonBox.accepted.connect(self.setZeroPoint) buttonBox.rejected.connect(self.reject) layout = QFormLayout() layout.addRow(self._listWidget) layout.addRow(buttonBox) self.setLayout(layout) def setZeroPoint(self): index = self._listWidget.indexFromItem( self._listWidget.selectedItems()[0]).row() self._zeroPointManager.setNewActiveZeroPoint(index) self.close()
class SimpleTodoPlus(QWidget): def __init__(self): QWidget.__init__(self) self.todo_list_widget = QListWidget(self) self.todo_list_widget.itemChanged.connect(self.onItemChanged) self.add_todo_btn = QPushButton("Add Todo", self) self.add_todo_btn.clicked.connect(self.add_todo_btn_clicked) self.remove_todo_btn = QPushButton("Remove Todo", self) self.remove_todo_btn.clicked.connect(self.remove_todo_btn_clicked) self.clear_todo_btn = QPushButton("Clear Todo", self) self.clear_todo_btn.clicked.connect(self.clear_todo_btn_clicked) vbox_layout = QVBoxLayout(self) vbox_layout.addWidget(self.todo_list_widget) hbox_layout = QHBoxLayout() hbox_layout.addWidget(self.add_todo_btn) hbox_layout.addWidget(self.remove_todo_btn) hbox_layout.addWidget(self.clear_todo_btn) vbox_layout.addLayout(hbox_layout) def add_todo_btn_clicked(self): item = QListWidgetItem(f"Todo {self.todo_list_widget.count() + 1}") item.setFlags(item.flags() | Qt.ItemIsUserCheckable | Qt.ItemIsEditable) item.setCheckState(Qt.Unchecked) self.todo_list_widget.addItem(item) self.todo_list_widget.edit(self.todo_list_widget.indexFromItem(item)) def remove_todo_btn_clicked(self): if self.todo_list_widget.count(): self.todo_list_widget.takeItem(self.todo_list_widget.currentRow()) def clear_todo_btn_clicked(self): self.todo_list_widget.clear() def onItemChanged(self, item): font = item.font() font.setStrikeOut(item.checkState() == Qt.Checked) item.setFont(font)
class VODCutter(QMainWindow): def __init__(self): QMainWindow.__init__(self) self.twitch_client_id = config.TWITCH_API_CLIENT_ID self.twitch_oauth_token = config.TWITCH_API_OAUTH_TOKEN self.twitch_interface = TwitchInterface( api_client_id=config.TWITCH_API_CLIENT_ID, api_oauth_token=config.TWITCH_API_OAUTH_TOKEN, browser_client_id=config.TWITCH_BROWSER_OAUTH_TOKEN, browser_oauth_token=config.TWITCH_BROWSER_OAUTH_TOKEN) self.vlc_interface = VLCInterface(config.VLC_PATH) self.loaded_video = None self.main_layout = QVBoxLayout() self.launch_vlc_btn = QPushButton("Launch VLC") self.info_layout = QGridLayout() self.file_picker_layout = QHBoxLayout() self.file_path_field = QLineEdit() self.file_browser_btn = QPushButton(text="...") self.file_picker_layout.addWidget(self.file_path_field) self.file_picker_layout.addWidget(self.file_browser_btn) vod_filepath_label = QLabel("VOD Filepath") id_twitch_label = QLabel("ID Twitch") created_at_label = QLabel("Created at") duration_label = QLabel("Duration") title_label = QLabel("Title") streamer_label = QLabel("Streamer") self.id_twitch_field = QLineEdit() self.created_at_field = QLineEdit() self.duration_field = QLineEdit() self.title_field = QLineEdit() self.streamer_field = QLineEdit() self.id_twitch_field.setEnabled(False) self.created_at_field.setEnabled(False) self.duration_field.setEnabled(False) self.title_field.setEnabled(False) self.streamer_field.setEnabled(False) self.info_layout.addWidget(vod_filepath_label, 0, 0) self.info_layout.addWidget(id_twitch_label, 1, 0) self.info_layout.addWidget(created_at_label, 2, 0) self.info_layout.addWidget(duration_label, 3, 0) self.info_layout.addWidget(title_label, 4, 0) self.info_layout.addWidget(streamer_label, 5, 0) self.info_layout.addLayout(self.file_picker_layout, 0, 1) self.info_layout.addWidget(self.id_twitch_field, 1, 1) self.info_layout.addWidget(self.created_at_field, 2, 1) self.info_layout.addWidget(self.duration_field, 3, 1) self.info_layout.addWidget(self.title_field, 4, 1) self.info_layout.addWidget(self.streamer_field, 5, 1) self.segments_create_btn = QPushButton("Import Chapters") self.download_thumbnails_btn = QPushButton("Download Thumbnails") self.download_chatlog_btn = QPushButton("Download Chat Log") self.segments_list = QListWidget() self.segments_add_btn = QPushButton(text="+") self.segments_delete_btn = QPushButton(text="-") self.jump_start_btn = QPushButton(text="Jump To Start") self.jump_end_btn = QPushButton(text="Jump To End") self.set_start_btn = QPushButton(text="Set Start") self.set_end_btn = QPushButton(text="Set End") self.split_btn = QPushButton(text="Split") self.process_selected_btn = QPushButton( text="Process Selected Segment") self.process_all_btn = QPushButton(text="Process All Segments") self.jump_layout = QHBoxLayout() self.jump_layout.addWidget(self.jump_start_btn) self.jump_layout.addWidget(self.jump_end_btn) self.set_layout = QHBoxLayout() self.set_layout.addWidget(self.set_start_btn) self.set_layout.addWidget(self.set_end_btn) self.main_layout.addWidget(self.launch_vlc_btn) self.main_layout.addLayout(self.file_picker_layout) self.main_layout.addLayout(self.info_layout) self.main_layout.addWidget(self.segments_create_btn) self.main_layout.addWidget(self.download_thumbnails_btn) self.main_layout.addWidget(self.download_chatlog_btn) self.main_layout.addWidget(self.segments_list) self.main_layout.addWidget(self.segments_add_btn) self.main_layout.addWidget(self.segments_delete_btn) self.main_layout.addLayout(self.jump_layout) self.main_layout.addLayout(self.set_layout) self.main_layout.addWidget(self.split_btn) self.main_layout.addWidget(self.process_selected_btn) self.main_layout.addWidget(self.process_all_btn) self.main_widget = QWidget() self.main_widget.setLayout(self.main_layout) self.setCentralWidget(self.main_widget) self.segments_list.itemDoubleClicked.connect( self.on_segments_list_doubleclick) self.jump_start_btn.clicked.connect(self.jump_to_segment_start) self.jump_end_btn.clicked.connect(self.jump_to_segment_end) self.set_start_btn.clicked.connect(self.set_segment_start) self.set_end_btn.clicked.connect(self.set_segment_end) self.download_thumbnails_btn.clicked.connect(self.download_thumbnails) self.segments_add_btn.clicked.connect(self.create_segment) self.segments_delete_btn.clicked.connect(self.delete_segment) self.split_btn.clicked.connect(self.split_selected_segment) self.launch_vlc_btn.clicked.connect(self.on_launch_vlc) self.file_path_field.returnPressed.connect(self.on_video_url_changed) self.file_browser_btn.clicked.connect(self.on_filebrowse_btn_click) self.process_selected_btn.clicked.connect( self.process_selected_segment) self.process_all_btn.clicked.connect(self.process_all_segments) def on_launch_vlc(self): self.vlc_interface.launch() def on_filebrowse_btn_click(self): filename = QFileDialog.getOpenFileName(self, "Select a video file") if filename[0]: self.set_video_file(filename[0]) def on_video_url_changed(self): self.set_video_file(self.file_path_field.text()) def on_segments_list_doubleclick(self, item): current_segment = item.get_segment() if current_segment: self.vlc_interface.set_current_time(int( current_segment.start_time)) def set_video_file(self, filepath=None): self.file_path_field.setText("" if filepath is None else filepath) if filepath: self.loaded_video = InputVideo() if re.search(r"^(?:/|[a-z]:[\\/])", filepath, re.I): file_url = "file://" + filepath self.loaded_video.is_local = True else: file_url = filepath if not self.loaded_video.is_local: streams = streamlink.streams(file_url) if streams: self.loaded_video.filepath = streams["best"].url else: self.loaded_video.filepath = file_url else: self.loaded_video.filepath = file_url try: self.update_twitch_metadatas() except requests.exceptions.ConnectionError: print("<!!> Can't connect to Twitch API.") try: self.vlc_interface.open_url(self.loaded_video.filepath) except requests.exceptions.ConnectionError: print("<!!> Can't connect to local VLC instance.") def get_twitch_id_from_filepath(self): filename = self.file_path_field.text() parsed_filename = re.search("([0-9]+)\.mp4$", filename, re.I) if parsed_filename: video_id = parsed_filename.group(1) return int(video_id) else: parsed_url = re.search("videos/([0-9]+)", filename, re.I) if parsed_url: video_id = parsed_url.group(1) return int(video_id) else: raise Exception( f"<!!> Can't find video Twitch id in video filename ({filename})" ) def create_segment_before(self, segment_obj): pass def create_segment_after(self, segment_obj): pass def update_twitch_metadatas(self): twitch_video_id = self.get_twitch_id_from_filepath() metadatas = self.twitch_interface.get_twitch_metadatas(twitch_video_id) self.loaded_video.metadatas = metadatas duration = parse_duration(metadatas["duration"]) self.id_twitch_field.setText(metadatas["id"]) self.created_at_field.setText(str(metadatas["created_at"])) self.duration_field.setText(format_time(duration.seconds)) self.title_field.setText(metadatas["title"]) self.streamer_field.setText(metadatas["user_login"]) for moment in self.twitch_interface.get_video_games_list( metadatas["id"]): s = Segment() s.name = f"{moment['description']} ({moment['type']})" s.start_time = moment['positionMilliseconds'] / 1000 s.end_time = (moment['positionMilliseconds'] + moment['durationMilliseconds']) / 1000 self.segments_list.addItem(SegmentListItem(s)) def create_segment(self): s = Segment() s.name = f"Segment {self.segments_list.count()}" s.start_time = 0 s.end_time = self.vlc_interface.get_duration() self.segments_list.addItem(SegmentListItem(s)) def delete_segment(self): for item in self.segments_list.selectedItems(): idx = self.segments_list.indexFromItem(item) item = self.segments_list.takeItem(idx.row()) del item def split_selected_segment(self): current_time = self.vlc_interface.get_current_time() for segment_item in self.segments_list.selectedItems(): current_segment = segment_item.get_segment() if current_segment: new_segment = segment_item.split( current_time, name="Splitted " + current_segment.name, split_mode=SPLIT_MODE.ABSOLUTE) self.segments_list.addItem(SegmentListItem(new_segment)) def get_selected_segments(self): return list( map(lambda item: item.get_segment(), self.segments_list.selectedItems())) def jump_to_segment_start(self): selected_segments = self.get_selected_segments() if selected_segments: self.vlc_interface.set_current_time( math.floor(selected_segments[0].start_time)) def jump_to_segment_end(self): selected_segments = self.get_selected_segments() if selected_segments: self.vlc_interface.set_current_time( math.floor(selected_segments[0].end_time)) def set_segment_start(self): current_time = self.vlc_interface.get_current_time() selected_segments = self.segments_list.selectedItems() if selected_segments: selected_segments[0].get_segment().start_time = current_time selected_segments[0].update() def set_segment_end(self): current_time = self.vlc_interface.get_current_time() selected_segments = self.segments_list.selectedItems() if selected_segments: selected_segments[0].get_segment().end_time = current_time selected_segments[0].update() def process_selected_segment(self): for segment in self.get_selected_segments(): self.process_segment(segment) def process_all_segments(self): for idx in range(self.segments_list.count()): segment_item = self.segments_list.item(idx) self.process_segment(segment_item.get_segment()) def process_segment(self, segment_obj): if not self.loaded_video: raise Exception("<!!> No video loaded") video_id = self.loaded_video.metadatas.get("id", None) created_at = self.loaded_video.metadatas.get("created_at", None) user_login = self.loaded_video.metadatas.get("user_login", None) if not (video_id and created_at and user_login): raise Exception("<!!> Missing video metadatas") created_at_timestamp = int(datetime.datetime.timestamp(created_at)) if self.loaded_video.is_local: cmd = f'ffmpeg -i "{self.loaded_video.filepath}" -ss {segment_obj.start_time} -to {segment_obj.end_time} -c:v copy -c:a copy "{user_login}_{created_at_timestamp}_{video_id}.mp4"' else: cmd = f'streamlink -f --hls-start-offset {format_time(segment_obj.start_time)} --hls-duration {format_time(segment_obj.end_time - segment_obj.start_time)} --player-passthrough hls "{self.loaded_video.filepath}" best -o "{user_login}_{created_at_timestamp}_{video_id}.mp4"' print(cmd) os.system(cmd) def download_thumbnails(self): twitch_video_id_str = self.id_twitch_field.text() if twitch_video_id_str: thumbnails_manifest_url = self.twitch_interface.get_video_thumbnails_manifest_url( int(twitch_video_id_str)) thumbnails_manifest, images_url_list = self.twitch_interface.get_thumbnails_url_from_manifest( thumbnails_manifest_url) for img in images_url_list: r = requests.get(images_url_list[img]) fp = open(img, "wb") fp.write(r.content) fp.close()
class ImageToPdfWidget(QWidget): def __init__(self, status_link): super(ImageToPdfWidget, self).__init__() LABEL_WIDTH = 80 self.last_selected_items = [] self.options_mode = 0 self.update_status_combobox = False layout = QHBoxLayout() self.status_bar = status_link self.list_view = QListWidget() self.list_view.setSelectionMode(QAbstractItemView.ExtendedSelection) self.list_view.setAlternatingRowColors(True) self.list_view.itemClicked.connect(self.click_item_signal) self.list_view.itemEntered.connect(self.click_item_signal) self.list_view.itemSelectionChanged.connect( self.change_selection_signal) controls_layout = QVBoxLayout() controls_layout.setAlignment(Qt.AlignTop) select_zone = SelectWidget(self.click_move_up, self.click_move_down, self.click_invert, self.click_delete) # options zone ------------------------------------- options_zone = QGroupBox("Options") self.options_zone_layout = QVBoxLayout() self.options_mode_combobox = QComboBox() self._add_items_to_mode_combobox(self.options_mode_combobox) options_mode_label = QLabel("Mode") options_mode_label.setMaximumWidth(LABEL_WIDTH) options_mode_layout = QHBoxLayout() options_mode_layout.addWidget(options_mode_label) options_mode_layout.addWidget(self.options_mode_combobox) self.options_zone_layout.addLayout(options_mode_layout) self.option_source_widget = OptionsFromSourceWidget( label_width=LABEL_WIDTH, status_bar=self.status_bar) self.options_a_widget = OptionsAWidget(label_width=LABEL_WIDTH, status_bar=self.status_bar) self.options_mode_combobox.currentIndexChanged.connect( self.change_options_mode_signal) self.change_options_mode_signal(self.options_mode) options_zone.setLayout(self.options_zone_layout) # Add files button and final structures --------------------------- add_file_button = QPushButton("Add Files") add_file_button.clicked.connect(self.click_add_files) controls_layout.addWidget(select_zone) controls_layout.addWidget(options_zone) controls_layout.addWidget(add_file_button) # image preview --------------------------------------------------- image_prev_layout = QVBoxLayout() image_prev_layout.setContentsMargins(0, 0, 4, 0) self.IMG_PREVIEW_WIDTH = 256 self.img_label = QLabel() self.img_label.setAlignment(Qt.AlignCenter) self.select_text = "Click item to preview" self.last_created_pix_path = "" self.img_label.setText(self.select_text) image_prev_layout.addWidget(self.img_label) # slider for the preview scale self.img_scale_slider = QSlider() self.img_scale_slider.setMinimum(6) self.img_scale_slider.setMaximum(2048) self.img_scale_slider.setValue(self.IMG_PREVIEW_WIDTH) self.img_scale_slider.setOrientation(Qt.Horizontal) self.img_scale_slider.valueChanged.connect( self.change_scale_slider_signal) image_prev_layout.addWidget(self.img_scale_slider) self._update_preview() layout.addLayout(image_prev_layout) layout.addWidget(self.list_view) layout.addLayout(controls_layout) self.setLayout(layout) self.update_status_combobox = True def _pillow_to_pixmap(self, img): if img.mode == "RGB": r, g, b = img.split() img = Image.merge("RGB", (b, g, r)) elif img.mode == "RGBA": r, g, b, a = img.split() img = Image.merge("RGBA", (b, g, r, a)) elif img.mode == "L": img = img.convert("RGBA") img2 = img.convert("RGBA") data = img2.tobytes("raw", "RGBA") qim = QImage(data, img.size[0], img.size[1], QImage.Format_ARGB32) pixmap = QPixmap.fromImage(qim) return pixmap def _update_preview(self): self.img_label.setMinimumWidth(self.IMG_PREVIEW_WIDTH) self.img_label.setMaximumWidth( max(self.IMG_PREVIEW_WIDTH, self.img_scale_slider.width())) if len(self.last_created_pix_path) > 0: img = Image.open(self.last_created_pix_path) img_pix = self._pillow_to_pixmap(img) img_pix = img_pix.scaled( QSize(self.IMG_PREVIEW_WIDTH, self.IMG_PREVIEW_WIDTH), Qt.KeepAspectRatio) self.img_label.setPixmap(img_pix) def _set_preview(self, img_path): if img_path is None: self.last_created_pix_path = "" self.img_label.setText(self.select_text) else: if img_path != self.last_created_pix_path: self.last_created_pix_path = img_path self._update_preview() def _add_items_to_mode_combobox(self, combobox): combobox.addItem("From Source") combobox.addItem("A4") # combobox.addItem("A5") # combobox.addItem("A6") # combobox.addItem("Letter") def _get_filtered_string(self, string): to_return_array = [] for s in string: if s.isdigit(): to_return_array.append(s) return "".join(to_return_array) def get_images_to_save(self): path_array = [] for i in range(self.list_view.count()): path_array.append(self.list_view.item(i).get_data()) return path_array def get_image_parameters(self): # return as dictionary if self.options_mode == 0: return { "mode": 0, "pixels": self.option_source_widget.get_pixel_value(), "margin": self.option_source_widget.get_margin_value(), "background": self.option_source_widget.get_background_value() } else: return { "mode": self.options_mode, "align": self.options_a_widget.get_align_value(), "margin": self.options_a_widget.get_margin_value(), "background": self.options_a_widget.get_background_value() } def add_items(self, array): added_names = [] for a in array: new_name = os.path.basename(a) new_item = ImageListItem(new_name, a) added_names.append(new_name) self.list_view.addItem(new_item) self.status_bar.showMessage("Add items: " + ", ".join(added_names)) def change_scale_slider_signal(self, value): self.IMG_PREVIEW_WIDTH = value self._update_preview() self.status_bar.showMessage("Set preview scale to " + str(value)) def click_item_signal(self, item): pass # self._set_preview(item.get_data()) # self.status_bar.showMessage("Select " + str(item.text())) def _get_first_new_index(self, current, last): for v in current: if v not in last: return v return current[0] def change_selection_signal(self): if len(self.list_view.selectedItems()) == 0: self._set_preview(None) # nothing selected else: selected_indexes = [ self.list_view.indexFromItem(sel).row() for sel in self.list_view.selectedItems() ] item = self.list_view.item( self._get_first_new_index(selected_indexes, self.last_selected_items)) self._set_preview(item.get_data()) self.status_bar.showMessage("Select " + str(item.text())) self.last_selected_items = selected_indexes def change_options_mode_signal(self, index): self.options_mode = index if self.options_mode == 0: self.options_zone_layout.removeWidget(self.options_a_widget) self.options_a_widget.setParent(None) self.options_zone_layout.addWidget(self.option_source_widget) else: self.options_zone_layout.removeWidget(self.option_source_widget) self.option_source_widget.setParent(None) self.options_zone_layout.addWidget(self.options_a_widget) if self.update_status_combobox: self.status_bar.showMessage( "Set combine mode to \"" + self.options_mode_combobox.itemText(index) + "\"") def resizeEvent(self, size): # self._update_preview() pass def click_move_up(self): selected = self.list_view.selectedItems() selected_indexes = [ self.list_view.indexFromItem(sel).row() for sel in selected ] selected_indexes.sort() if len(selected_indexes) > 0 and selected_indexes[0] > 0: for index in selected_indexes: prev_item = self.list_view.takeItem(index - 1) self.list_view.insertItem(index, prev_item) self.status_bar.showMessage("Move " + str(len(selected_indexes)) + " items") else: self.status_bar.showMessage("Nothing to move") def click_move_down(self): selected = self.list_view.selectedItems() selected_indexes = [ self.list_view.indexFromItem(sel).row() for sel in selected ] selected_indexes.sort() sel_count = len(selected_indexes) if len(selected_indexes) > 0 and selected_indexes[ sel_count - 1] < self.list_view.count() - 1: for i_index in range(sel_count): next_item = self.list_view.takeItem( selected_indexes[sel_count - i_index - 1] + 1) self.list_view.insertItem( selected_indexes[sel_count - i_index - 1], next_item) self.status_bar.showMessage("Move " + str(len(selected_indexes)) + " items") else: self.status_bar.showMessage("Nothing to move") def click_invert(self): selected = self.list_view.selectedItems() selected_indexes = [] for sel in selected: selected_indexes.append(self.list_view.indexFromItem(sel).row()) total_indexes = [i for i in range(self.list_view.count())] new_indexes = [] for i in total_indexes: if i not in selected_indexes: new_indexes.append(i) self.list_view.clearSelection() for i in new_indexes: self.list_view.item(i).setSelected(True) self.status_bar.showMessage("Invert selection: " + str(new_indexes)) def click_delete(self): selected = self.list_view.selectedItems() delete_names = [] for s in selected: s_index = self.list_view.indexFromItem(s).row() del_item = self.list_view.takeItem(s_index) delete_names.append(del_item.text()) if len(delete_names) == 0: self.status_bar.showMessage("Nothing to delete") else: self.status_bar.showMessage("Delete items: " + ", ".join(delete_names)) def click_add_files(self): files_dialog = QFileDialog() files_dialog.setNameFilter("Images (*.jpg *.jpeg *.bmp *.png *.tiff)") files_dialog.setFileMode(QFileDialog.ExistingFiles) if files_dialog.exec_(): files = files_dialog.selectedFiles() self.add_items(files)
class PiecesPlayer(QWidget): """ main widget of application (used as widget inside PiecesMainWindow) """ def __init__(self, parent): """ standard constructor: set up class variables, ui elements and layout """ # TODO: split current piece info into separate lineedits for title, album name and length # TODO: make time changeable by clicking next to the slider (not only # by dragging the slider) # TODO: add "about" action to open info dialog in new "help" menu # TODO: add option to loop current piece (?) # TODO: more documentation # TODO: add some "whole piece time remaining" indicator? (complicated) # TODO: implement a playlist of pieces that can be edited and enable # going back to the previous piece (also un- and re-shuffling?) # TODO: implement debug dialog as menu action (if needed) if not isinstance(parent, PiecesMainWindow): raise ValueError('Parent widget must be a PiecesMainWindow') super(PiecesPlayer, self).__init__(parent=parent) # -- declare and setup variables for storing information -- # various data self._set_str = '' # string of currently loaded directory sets self._pieces = {} # {<piece1>: [<files piece1 consists of>], ...} self._playlist = [] # list of keys of self._pieces (determines order) self._shuffled = True # needed for (maybe) reshuffling when looping # doc for self._history: # key: timestamp ('HH:MM:SS'), # value: info_str of piece that started playing at that time self._history = {} self._status = 'Paused' self._current_piece = {'title': '', 'files': [], 'play_next': 0} self._default_volume = 60 # in percent from 0 - 100 self._volume_before_muted = self._default_volume # set to true by self.__event_movement_ended and used by self.__update self._skip_to_next = False # vlc-related variables self._vlc_instance = VLCInstance() self._vlc_mediaplayer = self._vlc_instance.media_player_new() self._vlc_mediaplayer.audio_set_volume(self._default_volume) self._vlc_medium = None self._vlc_events = self._vlc_mediaplayer.event_manager() # -- create and setup ui elements -- # buttons self._btn_play_pause = QPushButton(QIcon(get_icon_path('play')), '') self._btn_previous = QPushButton(QIcon(get_icon_path('previous')), '') self._btn_next = QPushButton(QIcon(get_icon_path('next')), '') self._btn_volume = QPushButton(QIcon(get_icon_path('volume-high')), '') self._btn_loop = QPushButton(QIcon(get_icon_path('loop')), '') self._btn_loop.setCheckable(True) self._btn_play_pause.clicked.connect(self.__action_play_pause) self._btn_previous.clicked.connect(self.__action_previous) self._btn_next.clicked.connect(self.__action_next) self._btn_volume.clicked.connect(self.__action_volume_clicked) # labels self._lbl_current_piece = QLabel('Current piece:') self._lbl_movements = QLabel('Movements:') self._lbl_time_played = QLabel('00:00') self._lbl_time_left = QLabel('-00:00') self._lbl_volume = QLabel('100%') # needed so that everything has the same position # independent of the number of digits of volume self._lbl_volume.setMinimumWidth(55) # sliders self._slider_time = QSlider(Qt.Horizontal) self._slider_volume = QSlider(Qt.Horizontal) self._slider_time.sliderReleased.connect( self.__event_time_changed_by_user) self._slider_volume.valueChanged.connect(self.__event_volume_changed) self._slider_time.setRange(0, 100) self._slider_volume.setRange(0, 100) self._slider_volume.setValue(self._default_volume) self._slider_volume.setMinimumWidth(100) # other elements self._checkbox_loop_playlist = QCheckBox('Loop playlist') self._lineedit_current_piece = QLineEdit() self._lineedit_current_piece.setReadOnly(True) self._lineedit_current_piece.textChanged.connect( self.__event_piece_text_changed) self._listwidget_movements = QListWidget() self._listwidget_movements.itemClicked.connect( self.__event_movement_selected) # -- create layout and insert ui elements-- self._layout = QVBoxLayout(self) # row 0 (name of current piece) self._layout_piece_name = QHBoxLayout() self._layout_piece_name.addWidget(self._lbl_current_piece) self._layout_piece_name.addWidget(self._lineedit_current_piece) self._layout.addLayout(self._layout_piece_name) # rows 1 - 5 (movements of current piece) self._layout.addWidget(self._lbl_movements) self._layout.addWidget(self._listwidget_movements) # row 6 (time) self._layout_time = QHBoxLayout() self._layout_time.addWidget(self._lbl_time_played) self._layout_time.addWidget(self._slider_time) self._layout_time.addWidget(self._lbl_time_left) self._layout.addLayout(self._layout_time) # row 7 (buttons and volume) self._layout_buttons_and_volume = QHBoxLayout() self._layout_buttons_and_volume.addWidget(self._btn_play_pause) self._layout_buttons_and_volume.addWidget(self._btn_previous) self._layout_buttons_and_volume.addWidget(self._btn_next) self._layout_buttons_and_volume.addWidget(self._btn_loop) self._layout_buttons_and_volume.addSpacing(40) # distance between loop and volume buttons: min. 40, but stretchable self._layout_buttons_and_volume.addStretch() self._layout_buttons_and_volume.addWidget(self._btn_volume) self._layout_buttons_and_volume.addWidget(self._slider_volume) self._layout_buttons_and_volume.addWidget(self._lbl_volume) self._layout.addLayout(self._layout_buttons_and_volume) # -- setup hotkeys -- self._KEY_CODES_PLAY_PAUSE = [269025044] self._KEY_CODES_NEXT = [269025047] self._KEY_CODES_PREVIOUS = [269025046] self._keyboard_listener = keyboard.Listener(on_press=self.__on_press) self._keyboard_listener.start() QShortcut(QKeySequence('Space'), self, self.__action_play_pause) # -- various setup -- self._timer = QTimer(self) self._timer.timeout.connect(self.__update) self._timer.start(100) # update every 100ms self.setMinimumWidth(900) self.setMinimumHeight(400) # get directory set(s) input and set up self._pieces # (exec_ means we'll wait for the user input before continuing) DirectorySetChooseDialog(self, self.set_pieces_and_playlist).exec_() # skip to next movement / next piece when current one has ended self._vlc_events.event_attach(VLCEventType.MediaPlayerEndReached, self.__event_movement_ended) def __action_next(self): """ switches to next file in self._current_piece['files'] or to the next piece, if the current piece has ended """ reset_pause_after_current = False # current movement is last of the current piece if self._current_piece['play_next'] == -1: if len(self._playlist) == 0: # reached end of playlist if self._btn_loop.isChecked(): self._playlist = list(self._pieces.keys()) if self._shuffled: shuffle(self._playlist) return if self._status == 'Playing': self.__action_play_pause() self._current_piece['title'] = '' self._current_piece['files'] = [] self._current_piece['play_next'] = -1 self._lineedit_current_piece.setText('') self.__update_movement_list() self.parentWidget().update_status_bar( self._status, 'End of playlist reached.') return else: if self.parentWidget().get_exit_after_current(): self.parentWidget().exit() if self.parentWidget().get_pause_after_current(): self.__action_play_pause() reset_pause_after_current = True # reset of the menu action will be at the end of this # function, or else we won't stay paused self._current_piece['title'] = self._playlist.pop(0) self._current_piece['files'] = [ p[1:-1] for p in self._pieces[self._current_piece['title']] ] # some pieces only have one movement self._current_piece['play_next'] = \ 1 if len(self._current_piece['files']) > 1 else -1 self.__update_vlc_medium(0) self._lineedit_current_piece.setText( create_info_str(self._current_piece['title'], self._current_piece['files'])) self.__update_movement_list() self._history[datetime.now().strftime('%H:%M:%S')] = \ self._lineedit_current_piece.text() else: self.__update_vlc_medium(self._current_piece['play_next']) # next is last movement if self._current_piece['play_next'] == \ len(self._current_piece['files']) - 1: self._current_piece['play_next'] = -1 else: # there are at least two movements of current piece left self._current_piece['play_next'] += 1 if self._status == 'Paused' and not reset_pause_after_current: self.__action_play_pause() elif reset_pause_after_current: self.parentWidget().set_pause_after_current(False) else: self._vlc_mediaplayer.play() self.parentWidget().update_status_bar( self._status, f'{len(self._pieces) - len(self._playlist)}/{len(self._pieces)}') def __action_play_pause(self): """ (gets called when self._btn_play_pause is clicked) toggles playing/pausing music and updates everything as needed """ # don't do anything now (maybe end of playlist reached?) if self._current_piece['title'] == '': return if self._status == 'Paused': if not self._vlc_medium: self.__action_next() self._vlc_mediaplayer.play() self._btn_play_pause.setIcon(QIcon(get_icon_path('pause'))) self._status = 'Playing' else: self._vlc_mediaplayer.pause() self._btn_play_pause.setIcon(QIcon(get_icon_path('play'))) self._status = 'Paused' self.parentWidget().update_status_bar( self._status, f'{len(self._pieces) - len(self._playlist)}/{len(self._pieces)}') def __action_previous(self): """ (called when self._btn_previous ist clicked) goes back one movement of the current piece, if possible (cannot go back to previous piece) """ # can't go back to previous piece, but current one has no or one movement if len(self._current_piece['files']) <= 1: pass # currently playing first movement, so nothing to do as well elif self._current_piece['play_next'] == 1: pass else: # we can go back one movement # currently at last movement if self._current_piece['play_next'] == -1: # set play_next to last movement self._current_piece['play_next'] = \ len(self._current_piece['files']) - 1 else: # currently before last movement # set play_next to current movement self._current_piece['play_next'] -= 1 self._vlc_mediaplayer.stop() self.__update_vlc_medium(self._current_piece['play_next'] - 1) self._vlc_mediaplayer.play() def __action_volume_clicked(self): """ (called when self._btn_volume is clicked) (un)mutes volume """ if self._slider_volume.value() == 0: # unmute volume self._slider_volume.setValue(self._volume_before_muted) else: # mute volume self._volume_before_muted = self._slider_volume.value() self._slider_volume.setValue(0) def __event_movement_ended(self, event): """ (called when self._vlc_media_player emits a MediaPlayerEndReached event) sets self._skip_to_next to True so the next self.__update call will trigger self.__action_next """ self._skip_to_next = True def __event_movement_selected(self): """ (called when self._listwidget_movements emits itemClicked) skips to the newly selected movement """ index = self._listwidget_movements.indexFromItem( self._listwidget_movements.currentItem()).row() # user selected a movement different from the current one if index != self.__get_current_movement_index(): self._current_piece['play_next'] = index self.__action_next() def __event_piece_text_changed(self): """ (called when self._lineedit_current_piece emits textChanged) ensures that the user sees the beginning of the text in self._lineedit_current_piece (if text is too long, the end will be cut off and the user must scroll manually to see it) """ self._lineedit_current_piece.setCursorPosition(0) def __event_volume_changed(self): """ (called when value of self._slider_volume changes) updates text of self._lbl_volume to new value of self._slider_value and sets icon of self._btn_volume to a fitting one """ volume = self._slider_volume.value() self._lbl_volume.setText(f'{volume}%') if volume == 0: self._btn_volume.setIcon(QIcon(get_icon_path('volume-muted'))) elif volume < 34: self._btn_volume.setIcon(QIcon(get_icon_path('volume-low'))) elif volume < 67: self._btn_volume.setIcon(QIcon(get_icon_path('volume-medium'))) else: self._btn_volume.setIcon(QIcon(get_icon_path('volume-high'))) self._vlc_mediaplayer.audio_set_volume(volume) def __event_time_changed_by_user(self): """ (called when user releases self._slider_time) synchronizes self._vlc_mediaplayer's position to the new value of self._slider_time """ self._vlc_mediaplayer.set_position(self._slider_time.value() / 100) def __get_current_movement_index(self): """ returns the index of the current movement in self._current_piece['files'] """ play_next = self._current_piece['play_next'] if play_next == -1: return len(self._current_piece['files']) - 1 else: return play_next - 1 def __on_press(self, key): """ (called by self._keyboard_listener when a key is pressed) looks up key code corresponding to key and calls the appropriate action function """ try: # key is not always of the same type (why would it be?!) key_code = key.vk except AttributeError: key_code = key.value.vk if key_code in self._KEY_CODES_PLAY_PAUSE: self.__action_play_pause() elif key_code in self._KEY_CODES_NEXT: self.__action_next() elif key_code in self._KEY_CODES_PREVIOUS: self.__action_previous() def __update_movement_list(self): """ removes all items currently in self._listwidget_movements and adds everything in self._current_piece['files] """ # TODO: use ID3 title instead of filename while self._listwidget_movements.count() > 0: self._listwidget_movements.takeItem(0) files = self._current_piece['files'] if os_name == 'nt': # windows paths look different than posix paths # remove path to file, title number and .mp3 ending files = [i[i.rfind('\\') + 3:-4] for i in files] else: files = [i[i.rfind('/') + 4:-4] for i in files] self._listwidget_movements.addItems(files) def __update(self): """ (periodically called when self._timer emits timeout signal) updates various ui elements""" # -- select currently playing movement in self._listwidget_movements -- if self._listwidget_movements.count() > 0: self._listwidget_movements.item( self.__get_current_movement_index()).setSelected(True) # -- update text of self._lbl_time_played and self._lbl_time_left, # if necessary -- if self._vlc_medium: try: time_played = self._vlc_mediaplayer.get_time() medium_duration = self._vlc_medium.get_duration() # other values don't make sense (but do occur) if (time_played >= 0) and (time_played <= medium_duration): self._lbl_time_played.setText( get_time_str_from_ms(time_played)) else: self._lbl_time_played.setText(get_time_str_from_ms(0)) self._lbl_time_left.setText( f'-{get_time_str_from_ms(medium_duration - time_played)}') except OSError: # don't know why that occurs sometimes pass # -- update value of self._slider_time -- # don't reset slider to current position if user is dragging it if not self._slider_time.isSliderDown(): try: self._slider_time.setValue( self._vlc_mediaplayer.get_position() * 100) except OSError: # don't know why that occurs sometimes pass if self._skip_to_next: self._skip_to_next = False self.__action_next() def __update_vlc_medium(self, files_index): old_medium = self._vlc_medium self._vlc_medium = self._vlc_instance.media_new( self._current_piece['files'][files_index]) self._vlc_medium.parse() self._vlc_mediaplayer.set_media(self._vlc_medium) if old_medium: # only release if not None old_medium.release() def get_history(self): """ getter function for parent widget """ return self._history def get_set_str(self): """ getter function for parent widget """ return self._set_str if self._set_str != '' \ else 'No directory set loaded.' def set_pieces_and_playlist(self, pieces, playlist, set_str, shuffled): """ needed so that DirectorySetChooseDialog can set our self._pieces and self._playlist """ # just to be sure if isinstance(pieces, dict) and isinstance(playlist, list): self._vlc_mediaplayer.stop() self._set_str = set_str self._pieces = pieces self._playlist = playlist self._shuffled = shuffled self._current_piece['title'] = self._playlist.pop(0) self._current_piece['files'] = [ p.replace('"', '') for p in self._pieces[self._current_piece['title']] ] self._current_piece['play_next'] = \ 1 if len(self._current_piece['files']) > 1 else -1 self._lineedit_current_piece.setText( create_info_str(self._current_piece['title'], self._current_piece['files'])) self.__update_movement_list() self.__update_vlc_medium(0) self._history[datetime.now().strftime('%H:%M:%S')] = \ self._lineedit_current_piece.text() def exit(self): """ exits cleanly """ try: # don't know why that occurs sometimes self._vlc_mediaplayer.stop() self._vlc_mediaplayer.release() self._vlc_instance.release() except OSError: pass self._keyboard_listener.stop()
class SynthPdfWidget(QWidget): def __init__(self, status_bar): super(SynthPdfWidget, self).__init__() self.status_bar = status_bar layout = QHBoxLayout() self.list_view = QListWidget() self.list_view.setSelectionMode(QAbstractItemView.ExtendedSelection) self.list_view.setAlternatingRowColors(True) self.list_view.itemClicked.connect(self.click_item_signal) self.list_view.itemEntered.connect(self.click_item_signal) self.list_view.itemSelectionChanged.connect( self.change_selection_signal) controls_layout = QVBoxLayout() controls_layout.setAlignment(Qt.AlignTop) select_zone = SelectWidget(self.click_move_up, self.click_move_down, self.click_invert, self.click_delete) select_zone.setMinimumWidth(250) add_pages_button = QPushButton("Add Pages From PDF") add_pages_button.clicked.connect(self.add_pdf) controls_layout.addWidget(select_zone) controls_layout.addWidget(add_pages_button) layout.addWidget(self.list_view) layout.addLayout(controls_layout) self.setLayout(layout) def _add_pages_from_pdf(self, pdf_path): with open(pdf_path, "rb") as file: reader = PdfFileReader(file) file_name = os.path.basename(pdf_path) pages_count = reader.getNumPages() for i in range(pages_count): new_item = ImageListItem( "page " + str(i + 1) + " (" + file_name + ")", (pdf_path, i)) self.list_view.addItem(new_item) self.status_bar.showMessage("Add " + str(pages_count) + " pages") # ----------external methods from create command def extern_get_files_and_pages( self ): # return selected pages in the form {file1: [p1, p2, ...], file2: [p1, p2, ...], ...} to_return = {} if len(self.list_view.selectedItems() ) == 0: # nothing selected, add all pages items_count = self.list_view.count() for item_index in range(items_count): item = self.list_view.item(item_index) item_data = item.get_data() file_name = item_data[0] page_index = item_data[1] if file_name in to_return: to_return[file_name].append(page_index) else: to_return[file_name] = [page_index] else: for s in self.list_view.selectedItems(): s_data = s.get_data() file_name = s_data[0] page_index = s_data[1] if file_name in to_return: to_return[file_name].append(page_index) else: to_return[file_name] = [page_index] return to_return # ----------add button----------------------- def add_pdf(self): files_dialog = QFileDialog() files_dialog.setNameFilter("PDF (*.pdf)") files_dialog.setFileMode(QFileDialog.ExistingFiles) if files_dialog.exec_(): files = files_dialog.selectedFiles() for f_path in files: self._add_pages_from_pdf(f_path) # ----------List_view signals---------------- def click_item_signal(self, item): self.status_bar.showMessage("Select " + str(item.text())) def change_selection_signal(self): if len(self.list_view.selectedItems()) == 0: # self._set_preview(None) # nothing selected pass # ----------list_view commands--------------- def click_move_up(self): selected = self.list_view.selectedItems() selected_indexes = [ self.list_view.indexFromItem(sel).row() for sel in selected ] selected_indexes.sort() if len(selected_indexes) > 0 and selected_indexes[0] > 0: for index in selected_indexes: prev_item = self.list_view.takeItem(index - 1) self.list_view.insertItem(index, prev_item) self.status_bar.showMessage("Move " + str(len(selected_indexes)) + " items") else: self.status_bar.showMessage("Nothing to move") def click_move_down(self): selected = self.list_view.selectedItems() selected_indexes = [ self.list_view.indexFromItem(sel).row() for sel in selected ] selected_indexes.sort() sel_count = len(selected_indexes) if len(selected_indexes) > 0 and selected_indexes[ sel_count - 1] < self.list_view.count() - 1: for i_index in range(sel_count): next_item = self.list_view.takeItem( selected_indexes[sel_count - i_index - 1] + 1) self.list_view.insertItem( selected_indexes[sel_count - i_index - 1], next_item) self.status_bar.showMessage("Move " + str(len(selected_indexes)) + " items") else: self.status_bar.showMessage("Nothing to move") def click_invert(self): selected = self.list_view.selectedItems() selected_indexes = [] for sel in selected: selected_indexes.append(self.list_view.indexFromItem(sel).row()) total_indexes = [i for i in range(self.list_view.count())] new_indexes = [] for i in total_indexes: if i not in selected_indexes: new_indexes.append(i) self.list_view.clearSelection() for i in new_indexes: self.list_view.item(i).setSelected(True) self.status_bar.showMessage("Invert selection: " + str(new_indexes)) def click_delete(self): selected = self.list_view.selectedItems() delete_names = [] for s in selected: s_index = self.list_view.indexFromItem(s).row() del_item = self.list_view.takeItem(s_index) delete_names.append(del_item.text()) if len(delete_names) == 0: self.status_bar.showMessage("Nothing to delete") else: self.status_bar.showMessage("Delete items: " + ", ".join(delete_names))