/
VolcanoByte
/
vbCinema
Обзор
Документация
Войти
/
VolcanoByte
/
vbCinema
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
sources/uFormMain.py
1 277 строк
50 KB
Eugene Gavrilyuk
Update sync
13 июл 2026, 15:57
13 июл 2026, 15:57
24fdc53
Код
Авторство
О чём код?
import os.path import os.path import webbrowser from datetime import datetime as dt from functools import partial from re import findall from PyQt6.QtCore import (Qt, QItemSelectionModel, QByteArray, QThread, QEvent) from PyQt6.QtGui import (QAction, QPixmap, QShortcut, QKeySequence, QIcon, QKeyEvent) from PyQt6.QtWidgets import (QMainWindow, QMessageBox, QFileDialog, QGraphicsScene, QGraphicsView, QLabel, QWidget, QHBoxLayout, QInputDialog, QMenu) from pyperclip import paste from formMain import Ui_MainWindow from uConsts import (StoreConsts, AppConsts, PageConsts, ThemeConsts, SyncConsts) from uDB import db from uFormChoose import FormChoose from uFormOptions import FormOptions from uFormProperties import FormProperties from uFormSearch import FormSearch from uGlobal import (app, store, recentFiles, MovieColumns, MessageLanguageConsts as mlc) from uMovie import Movie from uSQL import SQL_TAG_SELECT_ALL_BY_NAME from uSwagger import (getInfo, getCover, getCoverFilename, getStuff, SwaggerException, getKeyword, getSimilar, getSequelsAndPrequels) from uThemes import lightPalette, darkPalette, darkStyleSheet, lightStyleSheet from uUtils import (confirmOkCancelDlg, confirmYesNoCancelDlg, errorDlg, PopUp, infoDlg, getFileNameDialog, getInt, getBool, getFloat, getDict, getColumnsSize, setColumnsSize) from uWebDavSync import WebDavSync class FormMain(QMainWindow, Ui_MainWindow): """Главная форма приложения""" def __init__(self): super().__init__() self.panelInfoVisible = None self.panelTagsVisible = None self.syncRunning = False self.thread = None self.worker = None self.checkWhere = None self.popup = None self.isChanged = False self.selectedMovie = None self.findWhat = None self.setupUi(self) self.setWindowTitle(AppConsts.Title + ' - ' + db.filename) self.edMovieId.installEventFilter(self) self.edMovieTags.installEventFilter(self) self.connectActions() self.connectChanges() # Настройка статусбара self.statusCount = QLabel() self.statusFilter = QLabel() self.statusMessage = QLabel() widget = QWidget() widget.setLayout(QHBoxLayout()) self.statusBar().addWidget(self.statusMessage) self.statusBar().addPermanentWidget(self.statusFilter) self.statusBar().addPermanentWidget(self.statusCount) # Контекстные меню self.filtersContextMenu = QMenu(self) self.moviesContextMenu = QMenu(self) self.tagsContextMenu = QMenu(self) self.connectContextMenu() # Настройка редактора тегов self.edMovieTags.setPlaceholderText(mlc.placeTags) self.edMovieTags.textEdited.connect(self.edMovieTagsTextEdited) self.readSettings() self.setColumnsVisible() self.updateAppearance() if self.lvTags.tagsSelected: self.refresh(db.queryTagsMovies(self.lvTags.tagsSelected)) else: self.refresh(db.queryAllMovies()) self.timer_id = self.startTimer(0) self.checkFresh('remote') def checkFresh(self, where: str): """ Проверка наличия свежей базы в фоновом режиме, """ # Проверяем, нужна ли проверка обновления if (where == 'remote' and not store.value( StoreConsts.OptionsUpdateStart, True) or where == 'local' and not store.value(StoreConsts.OptionsUpdateFinish, True)): return self.checkWhere = where self.thread = QThread() self.worker = WebDavSync(db.property('fldPropertySyncURL'), db.property('fldPropertySyncUsername'), db.property('fldPropertySyncPassword')) self.worker.moveToThread(self.thread) # Подключаем сигналы self.thread.started.connect(self.worker.check) self.worker.finished.connect(self.finishedCheck) self.worker.error.connect(self.task_error) self.worker.finished.connect(self.thread.quit) self.thread.finished.connect(self.cleanup_thread) # Запуск потока self.thread.start() self.syncRunning = True self.statusMessage.setText("Проверка свежей версии...") def finishedCheck(self): self.syncRunning = False self.statusMessage.setText(mlc.statusBarReady) if (self.checkWhere == 'remote' and WebDavSync.result == SyncConsts.YoungerBackup and confirmOkCancelDlg(self, mlc.syncFormBackup) == QMessageBox.StandardButton.Ok or self.checkWhere == 'local' and WebDavSync.result == SyncConsts.YoungerDB and confirmOkCancelDlg(self, mlc.syncFormLocal) == QMessageBox.StandardButton.Ok): self.on_fileSynchronize() def connectActions(self): """Подключение методов к слотам""" # QTableView и QListView self.tvMovies.editFunc = self.on_editModify self.tvMovies.runFunc = self.on_viewRunMovie self.tvMovies.viewPageKinopoiskFunc = self.on_viewPageKinopoisk self.tvMovies.viewPageIMDBFunc = self.on_viewPageIMDB self.tvMovies.selectionModel().selectionChanged.connect( self.on_selectMovie) self.lvTags.selectionModel().selectionChanged.connect( self.on_selectTag) self.lvTags.refreshFunc = self.refresh # QAction self.aiFileNewCollection.triggered.connect(self.on_fileNewCollection) self.aiFileOpenCollection.triggered.connect(self.on_fileOpenCollection) self.menuOpenRecent.aboutToShow.connect(self.populateOpenRecent) self.aiFileProperties.triggered.connect(self.on_fileProperties) self.aiFileSynchronize.triggered.connect(self.on_fileSynchronize) self.aiFileExit.triggered.connect(self.close) self.aiEditOptions.triggered.connect(self.on_editOptions) self.aiEditAddMovie.triggered.connect(self.on_editAdd) self.aiEditModifyMovie.triggered.connect(self.on_editModify) self.aiEditDeleteMovie.triggered.connect(self.on_editDelete) self.aiEditAddTag.triggered.connect(self.on_editTagAdd) self.aiEditEraseTag.triggered.connect(self.on_editTagErase) self.aiEditWanted.triggered.connect(self.on_editWanted) self.aiEditWatched.triggered.connect(self.on_editWatched) self.menuViewPanels.addAction(self.dockTags.toggleViewAction()) self.menuViewPanels.addAction(self.dockInfo.toggleViewAction()) self.menuViewPanels.addAction(self.tbStandard.toggleViewAction()) self.aiViewRunMovie.triggered.connect(self.on_viewRunMovie) self.aiViewPageKinopoisk.triggered.connect(self.on_viewPageKinopoisk) self.aiViewPageIMDB.triggered.connect(self.on_viewPageIMDB) self.aiViewClearSelection.triggered.connect(self.on_viewClearSelection) self.aiViewFiltersWantedMovies.triggered.connect( self.on_viewFiltersWantedMovies) self.aiViewFiltersWatchedMovies.triggered.connect( self.on_viewFiltersWatchedMovies) self.aiViewFiltersUnwatchedMovies.triggered.connect( self.on_viewFiltersUnWatchedMovies) self.aiViewClearFilters.triggered.connect(self.on_viewClearFilters) self.aiSearchFind.triggered.connect(self.on_searchFind) QShortcut(QKeySequence("Ctrl+F"), self.tvMovies, self.on_searchFind) self.aiSearchKeyword.triggered.connect(self.on_searchKeyword) self.aiSearchSequels.triggered.connect(self.on_searchSequels) self.aiSearchSimilar.triggered.connect(self.on_searchSimilar) # Кнопки self.btnTagAdd.pressed.connect(self.on_editTagAdd) self.btnTagErase.pressed.connect(self.on_editTagErase) self.btnFillInfo.pressed.connect( self.on_editFillInfo) self.btnDownloadCover.pressed.connect( self.on_editDownloadCower) self.btnConfirmChanges.pressed.connect( self.on_editChangesConfirm) self.btnCancelChanges.pressed.connect(self.on_editChangesCancel) def connectChanges(self): """ Отслеживание изменений при редактировании заметки""" self.edMovieId.textChanged.connect(self.setIsChanged) self.edMovieIdIMDB.textChanged.connect(self.setIsChanged) self.edMovieTitleOriginal.textChanged.connect(self.setIsChanged) self.edMovieTitleRussian.textChanged.connect(self.setIsChanged) self.edMovieTags.textChanged.connect(self.setIsChanged) self.sbMovieYear.textChanged.connect(self.setIsChanged) self.sbMovieLength.textChanged.connect(self.setIsChanged) self.sbMovieSequenceNumber.textChanged.connect(self.setIsChanged) self.sbMovieRatingKinopoisk.textChanged.connect(self.setIsChanged) self.sbMovieRatingIMDB.textChanged.connect(self.setIsChanged) self.teMovieDescription.textChanged.connect(self.setIsChanged) self.cbWanted.checkStateChanged.connect(self.setIsChanged) self.cbWatched.checkStateChanged.connect(self.setIsChanged) def connectContextMenu(self): # Меню фильмов self.moviesContextMenu.addAction(self.aiViewRunMovie) self.moviesContextMenu.addAction(self.aiViewPageKinopoisk) self.moviesContextMenu.addSeparator() self.moviesContextMenu.addAction(self.aiEditAddMovie) self.moviesContextMenu.addAction(self.aiEditModifyMovie) self.moviesContextMenu.addAction(self.aiEditDeleteMovie) self.moviesContextMenu.addSeparator() self.moviesContextMenu.addAction(self.aiEditWanted) self.moviesContextMenu.addAction(self.aiEditWatched) self.moviesContextMenu.addSeparator() self.moviesContextMenu.addAction(self.aiViewClearSelection) self.tvMovies.setContextMenuPolicy( Qt.ContextMenuPolicy.CustomContextMenu) self.tvMovies.customContextMenuRequested.connect( self.moviesContextMenuEvent) # Меню тегов self.tagsContextMenu.addAction(self.aiEditEraseTag) self.lvTags.setContextMenuPolicy( Qt.ContextMenuPolicy.CustomContextMenu) self.lvTags.customContextMenuRequested.connect( self.tagsContextMenuEvent) # Меню фильтров self.filtersContextMenu.addSeparator() self.filtersContextMenu.addAction(self.aiViewClearFilters) self.filtersContextMenu.addSeparator() self.filtersContextMenu.addAction(self.aiViewFiltersWatchedMovies) self.filtersContextMenu.addAction(self.aiViewFiltersUnwatchedMovies) self.filtersContextMenu.addAction(self.aiViewFiltersWantedMovies) self.statusFilter.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.statusFilter.customContextMenuRequested.connect( self.filterContextMenuEvent) def eventFilter(self, source, event: QKeyEvent): """Перехват клавиш в форме редактирования""" if event.type() == QEvent.Type.KeyPress: km = event.modifiers() # Перехват при вставке из буфера id фильма if source == self.edMovieId: # Ctrl-V или Shift-Ins if (event.key() == Qt.Key.Key_V and km == Qt.KeyboardModifier.ControlModifier or event.key() == Qt.Key.Key_Insert and km == Qt.KeyboardModifier.ShiftModifier): self.on_PasteId() return True # Перехват клавиш для управления и вставки тегов elif source == self.edMovieTags: # Shift-Return if (event.key() == Qt.Key.Key_Return and km == Qt.KeyboardModifier.ShiftModifier): self.on_TagsReturnPressed() return True # Shift-Up elif (event.key() == Qt.Key.Key_Up and km == Qt.KeyboardModifier.ShiftModifier): self.on_TagsComboUp() return True # Shift-Down elif (event.key() == Qt.Key.Key_Down and km == Qt.KeyboardModifier.ShiftModifier): self.on_TagsComboDown() return True return super().eventFilter(source, event) def moviesContextMenuEvent(self, point): self.moviesContextMenu.exec(self.tvMovies.mapToGlobal(point)) def tagsContextMenuEvent(self, point): self.tagsContextMenu.exec(self.lvTags.mapToGlobal(point)) def filterContextMenuEvent(self, point): self.filtersContextMenu.exec(self.statusFilter.mapToGlobal(point)) def timerEvent(self, event): """Обновляем состояние пунктов меню и кнопок""" isMovieSelected = bool(self.selectedMovie) isEditing = self.stackedWidget.currentIndex() == PageConsts.PageEdit isEditingHasId = bool(self.edMovieId.text()) isEditingHasTitle = bool(self.edMovieTitleOriginal.text()) isTagComboBoxEmpty = len(self.cbxTags.currentText().strip()) == 0 isCanRunMovie = bool(store.value(StoreConsts.OptionsViewingURL, '')) isIMDBPage = bool((self.selectedMovie and self.selectedMovie.data['fldMovieIMDBID'])) isToken = bool(store.value(StoreConsts.OptionsToken, '')) isSyncAvailable = (not self.syncRunning and bool(db.property('fldPropertySyncURL')) and bool(db.property('fldPropertySyncUsername')) and bool(db.property('fldPropertySyncPassword'))) self.widgetComboBoxTag.setEnabled(isMovieSelected and not isEditing) self.lvTags.setEnabled(not isEditing) self.aiFileSynchronize.setEnabled(isSyncAvailable and not isEditing) self.aiEditAddMovie.setEnabled(not isEditing) self.aiEditModifyMovie.setEnabled(isMovieSelected and not isEditing) self.aiEditDeleteMovie.setEnabled(isMovieSelected and not isEditing) self.aiEditWanted.setEnabled(isMovieSelected and not isEditing) if isMovieSelected and not isEditing: if getBool(self.selectedMovie.data['fldMovieWanted']): self.aiEditWanted.setIcon( QIcon(QPixmap("icons/wanted.png"))) self.aiEditWanted.setToolTip(mlc.wanted) else: self.aiEditWanted.setIcon( QIcon(QPixmap("icons/not_wanted.png"))) self.aiEditWanted.setToolTip(mlc.noWanted) self.aiEditWatched.setEnabled(isMovieSelected and not isEditing) if isMovieSelected and not isEditing: if getBool(self.selectedMovie.data['fldMovieWatched']): self.aiEditWatched.setIcon( QIcon(QPixmap("icons/watched.png"))) self.aiEditWatched.setToolTip(mlc.watched) else: self.aiEditWatched.setIcon( QIcon(QPixmap("icons/not_watched.png"))) self.aiEditWatched.setToolTip(mlc.noWatched) self.btnFillInfo.setEnabled(isToken and isEditingHasId and isEditing) self.btnDownloadCover.setEnabled(isToken and isEditingHasId and isEditing) self.btnConfirmChanges.setEnabled(isEditing and self.isChanged and isEditingHasId and isEditingHasTitle) self.btnCancelChanges.setEnabled(isEditing) self.aiEditAddTag.setEnabled(isMovieSelected and not isEditing and not isTagComboBoxEmpty) self.btnTagAdd.setEnabled(self.aiEditAddMovie.isEnabled()) self.aiEditEraseTag.setEnabled(isMovieSelected and not isEditing and not isTagComboBoxEmpty) self.btnTagErase.setEnabled(self.aiEditEraseTag.isEnabled()) self.tabWidget.setEnabled(isMovieSelected and not isEditing) self.aiViewRunMovie.setEnabled(isCanRunMovie and isMovieSelected and not isEditing) self.aiViewPageKinopoisk.setEnabled(isMovieSelected and not isEditing) self.aiViewPageIMDB.setEnabled(isIMDBPage and not isEditing) self.aiViewClearSelection.setEnabled(not isEditing) self.aiViewFiltersWantedMovies.setEnabled(not isEditing) self.aiViewFiltersWatchedMovies.setEnabled(not isEditing) self.aiViewFiltersUnwatchedMovies.setEnabled(not isEditing) self.aiSearchFind.setEnabled(not isEditing) self.aiSearchSimilar.setEnabled(isToken and not isEditing and isMovieSelected) self.aiSearchSequels.setEnabled(isToken and not isEditing and isMovieSelected) self.aiSearchKeyword.setEnabled(isToken and not isEditing) if not isMovieSelected: self.tbInfo.clear() self.gvCover.setScene(None) def writeSettings(self): """Сохранение настроек""" store.setValue(StoreConsts.MainFormGeometry, self.saveGeometry()) store.setValue(StoreConsts.MainFormState, self.saveState()) store.setValue(StoreConsts.MainFormColumnSizes, getColumnsSize(self.tvMovies)) # Видимость сохраняется в store при вызове Options store.setValue(StoreConsts.MainFormSortColumn, self.tvMovies.movieProxy.sortColumn) store.setValue(StoreConsts.MainFormSortOrder, self.tvMovies.movieProxy.sortOrder) store.setValue(StoreConsts.MainFormFilterIsWanted, self.tvMovies.movieProxy.isWanted) store.setValue(StoreConsts.MainFormFilterIsWatched, self.tvMovies.movieProxy.isWatched) if self.selectedMovie: store.setValue(StoreConsts.MainFormSelectedMovie, self.selectedMovie.id()) else: store.setValue(StoreConsts.MainFormSelectedMovie, None) store.setValue(StoreConsts.MainFromSelectedTags, self.lvTags.tagsSelected) def readSettings(self): """Загрузка настроек""" self.restoreGeometry(store.value( StoreConsts.MainFormGeometry, QByteArray())) self.restoreState(store.value( StoreConsts.MainFormState, QByteArray())) _sortColumn = getInt(store.value(StoreConsts.MainFormSortColumn, self.tvMovies.movieProxy.sortColumn)) _sortOrder = Qt.SortOrder(getInt(store.value( StoreConsts.MainFormSortOrder, self.tvMovies.movieProxy.sortOrder))) # Проверяем, если сортировка обратная, то учитываем if not MovieColumns[_sortColumn][5]: if _sortOrder == Qt.SortOrder.AscendingOrder: _sortOrder = Qt.SortOrder.DescendingOrder else: _sortOrder = Qt.SortOrder.AscendingOrder self.tvMovies.sortByColumn(_sortColumn, _sortOrder) _filterIsWanted = getBool(store.value( StoreConsts.MainFormFilterIsWanted, None)) _filterIsWatched = getBool(store.value( StoreConsts.MainFormFilterIsWatched, None)) self.setFilters(_filterIsWanted, _filterIsWatched) _selectedMovie = getInt(store.value(StoreConsts.MainFormSelectedMovie, None)) if _selectedMovie: self.selectedMovie = db.getMovie(_selectedMovie) else: self.selectedMovie = None _tags = store.value(StoreConsts.MainFromSelectedTags, set()) for _tag in _tags: self.lvTags.tagsSelected.add(_tag) sizeDefault = list(col[3] for col in MovieColumns) sizes = store.value(StoreConsts.MainFormColumnSizes, sizeDefault) setColumnsSize(self.tvMovies, sizes) def setColumnsVisible(self): visibleDefault = {col[0]: col[4] for col in MovieColumns} visibilities = store.value(StoreConsts.MainFormColumnVisibilities, visibleDefault) for column in range(len(MovieColumns)): field = MovieColumns[column][0] hidden = not field in visibilities or not visibilities[field] self.tvMovies.setColumnHidden(column, hidden) if not hidden and self.tvMovies.columnWidth(column) == 0: self.tvMovies.setColumnWidth(column, MovieColumns[column][2]) def updateAppearance(self): gridStyle = store.value(StoreConsts.OptionsGridStyle, AppConsts.DefaultGridStyle) self.tvMovies.setGridStyle(Qt.PenStyle(int(gridStyle))) theme = store.value(StoreConsts.OptionsTheme, AppConsts.DefaultTheme) if theme == ThemeConsts.Dark: app.setPalette(darkPalette) self.lvTags.setStyleSheet(darkStyleSheet) else: app.setPalette(lightPalette) self.lvTags.setStyleSheet(lightStyleSheet) def hideEvent(self, event): self.panelInfoVisible = self.dockInfo.isVisible() self.panelTagsVisible = self.dockTags.isVisible() def showEvent(self, event): if self.panelInfoVisible: self.dockInfo.setVisible(self.panelInfoVisible) if self.panelTagsVisible: self.dockTags.setVisible(self.panelTagsVisible) def closeEvent(self, event): # self.checkFresh('local') self.writeSettings() super().closeEvent(event) event.accept() def selectMovie(self, movieId): # Выделяем фильм _start = self.tvMovies.movieProxy.index(0, 0) _matches = self.tvMovies.movieProxy.match( _start, Qt.ItemDataRole.DisplayRole, movieId, 1, Qt.MatchFlag.MatchExactly) if _matches: _index = _matches[0] self.tvMovies.selectionModel().clearSelection() self.tvMovies.selectionModel().setCurrentIndex( _index, QItemSelectionModel.SelectionFlag.Select | QItemSelectionModel.SelectionFlag.Rows) def getMovieIds(self) -> set: """Возвращает множество с выбранным фильмов или со всеми видимыми фильмами""" if self.selectedMovie: return {self.selectedMovie.id()} else: return self.getVisibleMovies() def getVisibleMovies(self) -> set: res = set() for _row in range(self.tvMovies.movieProxy.rowCount()): _index = (self.tvMovies.movieProxy.index(_row, 0).data()) res.add(_index) return res def getSelectedTag(self, selected) -> str: """Возвращает выбранный тег в ListView""" row = selected.indexes()[0].row() res = self.lvTags.model().index(row, 1).data() return res def refresh(self, query: str = None, pageIfNoRow: int = PageConsts.PageWelcome): """Обновление моделей списка заметок (и тегов)""" if query: self.tvMovies.movieQuery = query self.tvMovies.movieModel.setQuery(self.tvMovies.movieQuery) while self.tvMovies.movieModel.canFetchMore(): self.tvMovies.movieModel.fetchMore() if self.tvMovies.movieModel.rowCount() == 0: self.stackedWidget.setCurrentIndex(pageIfNoRow) else: self.stackedWidget.setCurrentIndex(PageConsts.PageView) if self.tvMovies.movieProxy.isWatched is not None: if self.tvMovies.movieProxy.isWatched: filters = mlc.filterWatched else: filters = mlc.filterUnwatched else: filters = mlc.filterAllMovies if self.tvMovies.movieProxy.isWanted: filters = filters + ' ' + mlc.filterWanted else: filters = filters + ' ' + mlc.filterMovies self.statusFilter.setText(filters) self.statusCount.setText(mlc.statusBarCounter % ( self.tvMovies.model().rowCount(), self.lvTags.model().rowCount())) self.statusMessage.setText(mlc.statusBarReady) _dt = dt.fromtimestamp(db.timestamp()) self.setWindowTitle(f"{AppConsts.Title} - {db.syncId} " f"[{dt.strftime(_dt, '%Y-%m-%d %H-%M')}]") self.refreshTags() if self.selectedMovie: self.selectMovie(self.selectedMovie.id()) def refreshTags(self): """Обновление модели списка тегов""" _ids = self.getMovieIds() self.lvTags.tagModel.setQuery(db.queryMoviesTags(_ids)) while self.lvTags.tagModel.canFetchMore(): self.lvTags.tagModel.fetchMore() self.cbxTags.tagComboBoxModel.setQuery(SQL_TAG_SELECT_ALL_BY_NAME) self.cbxTags.clearEditText() self.cbxMovieTags.tagComboBoxModel.setQuery(SQL_TAG_SELECT_ALL_BY_NAME) self.cbxTags.clearEditText() def updateMovieList(self, movieId: int = None): """Обновление списка фильмов (если указан movieId, то выделяем его)""" self.refresh() if movieId: self.selectMovie(movieId) def showCover(self, movieId): if os.path.exists(getCoverFilename(movieId)): cover = getCoverFilename(movieId) else: cover = 'NoCover.jpg' pixmap = QPixmap(cover) scaled = pixmap.scaled(self.gvCover.width(), self.gvCover.height(), aspectRatioMode=Qt.AspectRatioMode.KeepAspectRatio, transformMode=Qt.TransformationMode.SmoothTransformation) scene = QGraphicsScene() scene.addPixmap(scaled) self.gvCover.setScene(scene) def fillPageView(self, movie: Movie): """Заполнение текста и изображения""" self.tbInfo.setText(movie.data['fldMovieDesc']) self.showCover(movie.id()) def fillPageEdit(self, movie: Movie): self.edMovieId.setEnabled(False) self.edMovieId.setText(str(movie.data['fldMovieID'])) if 'fldMovieIMDBID' in movie.data: self.edMovieIdIMDB.setText(movie.data['fldMovieIMDBID']) if 'fldMovieOriginalTitle' in movie.data: self.edMovieTitleOriginal.setText( movie.data['fldMovieOriginalTitle']) if 'fldMovieRussianTitle' in movie.data: self.edMovieTitleRussian.setText( movie.data['fldMovieRussianTitle']) if 'fldMovieYear' in movie.data: self.sbMovieYear.setValue(getInt((movie.data['fldMovieYear']))) if 'fldMovieLength' in movie.data: self.sbMovieLength.setValue(getInt(movie.data['fldMovieLength'])) if 'fldMovieRatingKinopoisk' in movie.data: self.sbMovieRatingKinopoisk.setValue( getFloat(movie.data['fldMovieRatingKinopoisk'])) if 'fldMovieRatingIMDB' in movie.data: self.sbMovieRatingIMDB.setValue( getFloat(movie.data['fldMovieRatingIMDB'])) if 'fldMovieSequenceNumber' in movie.data: self.sbMovieSequenceNumber.setValue( getInt(movie.data['fldMovieSequenceNumber'])) if 'fldMovieDesc' in movie.data: self.teMovieDescription.setPlainText(movie.data['fldMovieDesc']) if 'fldMovieWatched' in movie.data: self.cbWatched.setChecked(getBool(movie.data['fldMovieWatched'])) if 'fldMovieWanted' in movie.data: self.cbWanted.setChecked(getBool(movie.data['fldMovieWanted'])) self.edMovieTags.setText(movie.tagsToString()) self.showCover(movie.id()) self.isChanged = False def clearPageEdit(self): self.edMovieId.setEnabled(True) self.edMovieId.clear() self.edMovieIdIMDB.clear() self.edMovieTitleOriginal.clear() self.edMovieTitleRussian.clear() self.edMovieTags.clear() self.sbMovieYear.clear() self.sbMovieLength.clear() self.sbMovieRatingKinopoisk.clear() self.sbMovieRatingIMDB.clear() self.sbMovieSequenceNumber.clear() self.teMovieDescription.clear() self.cbWatched.setChecked(False) self.cbWanted.setChecked(False) self.isChanged = False def on_PasteId(self): string = paste() number = findall(r'\d+.\d+', string) if number: self.edMovieId.setText(number[0]) def on_TagsComboUp(self): _index = self.cbxMovieTags.currentIndex() if _index != 0: self.cbxMovieTags.setCurrentIndex(_index - 1) def on_TagsComboDown(self): _index = self.cbxMovieTags.currentIndex() if _index != self.cbxMovieTags.count() - 1: self.cbxMovieTags.setCurrentIndex(_index + 1) def on_TagsReturnPressed(self): """Замена тега в позиции курсора""" # Проверяем, нет ли уже такого тега tags = [x.strip() for x in self.edMovieTags.text().split(',')] if self.cbxMovieTags.currentText() in tags: return pos = self.edMovieTags.cursorPosition() tags = self.edMovieTags.text().split(',') tagIndex = self.getTagInPosition(tags, pos) if tagIndex is not None: tags[tagIndex] = self.cbxMovieTags.currentText() self.edMovieTags.setText(', '.join([x.strip() for x in tags])) def edMovieTagsTextEdited(self, text): """Определение положения курсора в списке тегов и выбор подходящего в выпадающем списке.""" pos = self.edMovieTags.cursorPosition() tags = text.split(',') tagIndex = self.getTagInPosition(tags, pos) _tag = tags[tagIndex].strip() if _tag: _comboIndex = self.cbxMovieTags.findText( _tag, Qt.MatchFlag.MatchStartsWith) if _comboIndex >= 0: self.cbxMovieTags.setCurrentIndex(_comboIndex) def cbxMovieTagsReturnPressed(self): """Добавление тега в edMovieTags из cbxMovieTags, если такого нет""" tags = [x.strip() for x in self.edMovieTags.text().split(',')] tag = self.cbxMovieTags.currentText() if len(tags) == 0: self.edMovieTags.setText(tag) elif self.cbxMovieTags.currentText() in tags: return else: self.edMovieTags.setText(self.edMovieTags.text() + ', ' + tag) @staticmethod def getTagInPosition(tags: list, pos: int) -> int | None: """Определение тега в списке по указанному положению курсора.""" _len = 1 _index = 0 for tag in tags: _len += len(tag) if pos < _len: return _index _len += 1 _index += 1 def on_selectMovie(self, selected): """Реализация события выбора фильма""" if selected: row = selected.indexes()[0].row() _id = self.tvMovies.movieProxy.index(row, 0).data() self.selectedMovie = db.getMovie(_id) self.fillPageView(self.selectedMovie) self.stackedWidget.setCurrentIndex(PageConsts.PageView) else: self.selectedMovie = None # Пересчитываем теги _ids = self.getMovieIds() self.lvTags.tagModel.setQuery(db.queryMoviesTags(_ids)) def on_selectTag(self, selected): """Реализация события выбора тега""" if selected: tag = self.getSelectedTag(selected) self.cbxTags.setCurrentText(tag) def openDB(self, filename: str): db.open(filename) recentFiles.add(filename) store.setValue(StoreConsts.DBFileName, filename) self.refresh() # self.checkFresh() def on_fileNewCollection(self): """Создание новой коллекции""" filename = getFileNameDialog('New collection', QFileDialog.AcceptMode.AcceptSave, QFileDialog.FileMode.AnyFile) if filename is not None: db.close() if os.path.exists(filename): os.remove(filename) self.openDB(filename) def on_fileOpenCollection(self): """Открытие коллекции""" filename = getFileNameDialog('Open collection', QFileDialog.AcceptMode.AcceptOpen, QFileDialog.FileMode.ExistingFile) if filename is not None: self.on_fileOpenRecent(filename) def on_fileOpenRecent(self, filename): if not os.path.exists(filename): if (confirmOkCancelDlg(self, mlc.deleteRecentConfirm) == QMessageBox.StandardButton.Ok): recentFiles.remove(filename) else: return else: db.close() self.selectedMovie = None self.openDB(filename) def populateOpenRecent(self): # Шаг 1. Удаляем старые пункты меню self.menuOpenRecent.clear() # Шаг 2. Динамически создаем действия actions = [] for filename in recentFiles.recentFiles: action = QAction(filename, self) action.triggered.connect(partial(self.on_fileOpenRecent, filename)) actions.append(action) # Шаг 3. Добавляем действия к меню self.menuOpenRecent.addActions(actions) def on_fileProperties(self): winProperties = FormProperties(self) winProperties.exec() def on_fileSynchronize(self): """Запускает фоновую задачу в новом потоке.""" # Создаём поток и рабочий объект self.thread = QThread() self.worker = WebDavSync(db.property('fldPropertySyncURL'), db.property('fldPropertySyncUsername'), db.property('fldPropertySyncPassword')) self.worker.moveToThread(self.thread) # Подключаем сигналы self.thread.started.connect(self.worker.run) self.worker.finished.connect(self.finished) self.worker.error.connect(self.task_error) self.worker.finished.connect(self.thread.quit) self.thread.finished.connect(self.cleanup_thread) # Запуск потока self.thread.start() self.syncRunning = True self.statusMessage.setText("Синхронизация...") def finished(self): self.syncRunning = False # Как закончили синхронизацию, нужно переоткрыть базу if WebDavSync.result: db.close() db.reopen() self.refresh() self.statusMessage.setText(mlc.statusBarReady) self.statusbar.showMessage(WebDavSync.result, 5000) def task_error(self, error_msg): """Обработка ошибки или остановки задачи.""" self.syncRunning = False self.statusMessage.setText(mlc.statusBarReady) self.statusbar.showMessage(f"Ошибка: {error_msg}", 5000) def cleanup_thread(self): """Очистка после завершения потока.""" if self.thread: self.thread.deleteLater() self.thread = None if self.worker: self.worker.deleteLater() self.worker = None # Если поток завершён не из-за остановки пользователем, просто сбросим флаги if self.syncRunning: self.syncRunning = False def close_popup(self): self.popup.close() def show_popup(self): self.popup.exec() def on_editAdd(self): """Добавление новой заметки""" self.selectedMovie = None self.clearPageEdit() self.stackedWidget.setCurrentIndex(PageConsts.PageEdit) self.edMovieId.setFocus() def setIsChanged(self): self.isChanged = True def on_editModify(self): """Изменение выделенной заметки""" self.fillPageEdit(self.selectedMovie) self.stackedWidget.setCurrentIndex(PageConsts.PageEdit) self.edMovieTitleOriginal.setFocus() self.isChanged = False def on_editDelete(self): """Удаление фильма.""" if confirmOkCancelDlg(self, mlc.deleteConfirm) == QMessageBox.StandardButton.Ok: db.deleteMovie(self.selectedMovie.id()) self.lvTags.checkIsTagsExists() def on_editTagAdd(self): _tag = self.cbxTags.currentText() _id = self.selectedMovie.id() db.addMovieTag(_id, _tag) self.refreshTags() def on_editTagErase(self): _tag = self.cbxTags.currentText() _id = self.selectedMovie.id() db.deleteMovieTag(_id, _tag) self.refreshTags() def on_editWanted(self): if self.selectedMovie.data['fldMovieWanted']: self.selectedMovie.data['fldMovieWanted'] = 0 else: self.selectedMovie.data['fldMovieWanted'] = 1 db.setWanted(self.selectedMovie.id(), self.selectedMovie.data['fldMovieWanted']) if self.tvMovies.movieProxy.isWanted: self.refresh() def on_editWatched(self): if self.selectedMovie.data['fldMovieWatched']: self.selectedMovie.data['fldMovieWatched'] = 0 else: self.selectedMovie.data['fldMovieWatched'] = 1 db.setWatched(self.selectedMovie.id(), self.selectedMovie.data['fldMovieWatched']) if self.tvMovies.movieProxy.isWatched is not None: self.refresh() def on_editOptions(self): winOptions = FormOptions(self) if winOptions.exec(): self.updateAppearance() self.setColumnsVisible() @staticmethod def fillMovieInfo(idMovie: str) -> Movie: info = getInfo(idMovie) movie = Movie() movie.data['fldMovieID'] = idMovie movie.data['fldMovieIMDBID'] = getDict(info, 'imdbId', '') movie.data['fldMovieOriginalTitle'] = getDict(info, 'nameOriginal', '') movie.data['fldMovieRussianTitle'] = getDict(info, 'nameRu', '') if not movie.data['fldMovieOriginalTitle']: movie.data['fldMovieOriginalTitle'] = movie.data[ 'fldMovieRussianTitle'] if not movie.data['fldMovieRussianTitle']: movie.data['fldMovieRussianTitle'] = movie.data[ 'fldMovieOriginalTitle'] movie.data['fldMovieRatingKinopoisk'] = getDict(info, 'ratingKinopoisk', 0.0) movie.data['fldMovieRatingIMDB'] = getDict(info, 'ratingImdb', 0.0) movie.data['fldMovieYear'] = getDict(info, 'year', 0) movie.data['fldMovieLength'] = getDict(info, 'filmLength', 0) movie.data['fldMovieDesc'] = getDict(info, 'description', '') # собираем теги for c in info['countries']: movie.tags.add(c['country']) for g in info['genres']: movie.tags.add(g['genre']) if info['serial']: movie.tags.add('сериал') getCover(idMovie, getDict(info, 'posterUrlPreview', '')) stuff = getStuff(idMovie) actorsCount = getInt(store.value(StoreConsts.OptionsActorsCount, AppConsts.DefaultActorsCount)) directorsCount = getInt(store.value(StoreConsts.OptionsDirectorsCount, AppConsts.DefaultDirectorsCount)) nameLanguage = getInt(store.value(StoreConsts.OptionsNameLanguage, AppConsts.DefaultNameLanguage)) for person in stuff: if directorsCount and person['professionKey'] == 'DIRECTOR': if nameLanguage == 0: movie.tags.add(person['nameRu']) else: movie.tags.add(person['nameEn']) directorsCount -= 1 elif actorsCount and person['professionKey'] == 'ACTOR': if nameLanguage == 0: movie.tags.add(person['nameRu']) else: movie.tags.add(person['nameEn']) actorsCount -= 1 return movie def on_editFillInfo(self): idMovie = self.edMovieId.text() tags = Movie.stringToTags(self.edMovieTags.text()) try: movie = self.fillMovieInfo(idMovie) except SwaggerException as e: errorDlg(self, e.message) return for tag in tags: movie.tags.add(tag) self.fillPageEdit(movie) self.isChanged = True def on_editDownloadCower(self): idMovie = self.edMovieId.text() if (not os.path.exists(getCoverFilename(idMovie)) or confirmOkCancelDlg(self, mlc.downloadCoverConfirm) == QMessageBox.StandardButton.Ok): try: getCover(idMovie) self.showCover(idMovie) except Exception as e: print(e) def on_editChangesConfirm(self): """Подтверждение изменения фильма""" _id = int(self.edMovieId.text()) # Проверяем, не пытаемся ли мы добавить существующий фильм if not self.selectedMovie and db.getMovie(_id): if (confirmOkCancelDlg(self, mlc.movieAlreadyExists) == QMessageBox.StandardButton.Ok): self.lvTags.clearTags() self.stackedWidget.setCurrentIndex(PageConsts.PageView) self.selectMovie(_id) return movie = Movie() movie.data['fldMovieID'] = _id movie.data['fldMovieIMDBID'] = self.edMovieIdIMDB.text() movie.data['fldMovieOriginalTitle'] = self.edMovieTitleOriginal.text() movie.data['fldMovieRussianTitle'] = self.edMovieTitleRussian.text() movie.data['fldMovieYear'] = self.sbMovieYear.value() movie.data['fldMovieLength'] = self.sbMovieLength.value() movie.data[ 'fldMovieRatingKinopoisk'] = self.sbMovieRatingKinopoisk.value() movie.data['fldMovieRatingIMDB'] = self.sbMovieRatingIMDB.value() movie.data['fldMovieDesc'] = self.teMovieDescription.toPlainText() movie.data['fldMovieWatched'] = self.cbWatched.isChecked() movie.data['fldMovieWanted'] = self.cbWanted.isChecked() movie.data['fldMovieSequenceNumber'] = ( self.sbMovieSequenceNumber.value()) movie.tags = Movie.stringToTags(self.edMovieTags.text()) if self.selectedMovie: db.updateMovie(_id, movie) else: db.addMovie(movie) db.prepareTags() self.lvTags.clearTags() self.updateMovieList(_id) def on_editChangesCancel(self): """Отмена изменения фильма""" if self.isChanged: button = confirmYesNoCancelDlg(self, mlc.changeConfirm) if button == QMessageBox.StandardButton.Yes: self.on_editChangesConfirm() elif button == QMessageBox.StandardButton.Cancel: return if db.getRecCount('tblMovies'): self.stackedWidget.setCurrentIndex(PageConsts.PageView) else: self.stackedWidget.setCurrentIndex(PageConsts.PageWelcome) def on_viewClearSelection(self): """Сброс выделения фильтров, тегов и фильма""" if bool(self.selectedMovie): self.selectedMovie = None elif self.tvMovies.movieProxy.isFilters(): self.on_viewClearFilters() else: self.lvTags.clearTags() self.refresh() def on_viewRunMovie(self): if bool(store.value(StoreConsts.OptionsViewingURL, '')): _id = self.selectedMovie.id() url = store.value(StoreConsts.OptionsViewingURL, '') try: webbrowser.open(url % _id) except TypeError: errorDlg(self, 'Address format error') db.setMovieWatchedNow(_id) self.refresh() def on_viewPageKinopoisk(self): _id = self.selectedMovie.data['fldMovieID'] url = f'https://www.kinopoisk.ru/film/{_id}/' webbrowser.open(url) def on_viewPageIMDB(self): if bool(self.selectedMovie.data['fldMovieIMDBID']): _id = self.selectedMovie.data['fldMovieIMDBID'] url = f'https://www.imdb.com/title/{_id}/' webbrowser.open(url) def setFilters(self, isWanted, isWatched): self.aiViewFiltersWantedMovies.setChecked(bool(isWanted)) self.aiViewFiltersWatchedMovies.setChecked( isWatched is not None and isWatched) self.aiViewFiltersUnwatchedMovies.setChecked(isWatched is not None and not isWatched) self.tvMovies.movieProxy.isWanted = isWanted self.tvMovies.movieProxy.isWatched = isWatched def on_viewClearFilters(self): """Сброс фильтров""" if self.tvMovies.movieProxy.isFilters(): self.tvMovies.movieProxy.clearFilters() self.aiViewFiltersWantedMovies.setChecked(False) self.aiViewFiltersWatchedMovies.setChecked(False) self.aiViewFiltersUnwatchedMovies.setChecked(False) self.refresh() def on_viewFiltersWantedMovies(self): self.tvMovies.movieProxy.isWanted = ( self.aiViewFiltersWantedMovies.isChecked()) self.refresh() def on_viewFiltersWatchedMovies(self): if self.aiViewFiltersWatchedMovies.isChecked(): self.aiViewFiltersUnwatchedMovies.setChecked(False) self.tvMovies.movieProxy.isWatched = True else: self.tvMovies.movieProxy.isWatched = None self.refresh() def on_viewFiltersUnWatchedMovies(self): pass if self.aiViewFiltersUnwatchedMovies.isChecked(): self.aiViewFiltersWatchedMovies.setChecked(False) self.tvMovies.movieProxy.isWatched = False else: self.tvMovies.movieProxy.isWatched = None self.refresh() def on_searchFind(self): winSearch = FormSearch(self) if self.findWhat: winSearch.cbxWhat.setCurrentText(self.findWhat) if winSearch.exec(): self.findWhat = winSearch.cbxWhat.currentText() where = set() if store.value(StoreConsts.SearchMoveId, False): where.add('fldMovieID') if store.value(StoreConsts.SearchIMDBId, False): where.add('fldMovieIMDBID') if store.value(StoreConsts.SearchOriginalTitle, False): where.add('fldMovieOriginalTitle') if store.value(StoreConsts.SearchRussianTitle, False): where.add('fldMovieRussianTitle') if store.value(StoreConsts.SearchDescription, False): where.add('fldMovieDesc') query = ('SELECT * FROM tblMovies WHERE fldMovieID IN ' + db.find(self.findWhat, where)) self.refresh(query, PageConsts.PageSearchNotFound) def chooseMovies(self, caption: str, movies: list): """Добавляет выбранные фильмы, если их нет в базе, заполняя информацией из сваггера""" ids = set() winChoose = FormChoose(self, caption, movies) if winChoose.exec(): for i in range(winChoose.twMovies.rowCount()): item = winChoose.twMovies.item(i, 0) if item.checkState() == Qt.CheckState.Checked: movieId = winChoose.twMovies.item(i, 1).text() if db.getMovie(movieId) is None: ids.add(movieId) if ids: for _id in ids: try: movie = self.fillMovieInfo(_id) db.addMovie(movie) except SwaggerException as e: errorDlg(self, e.message) return self.refresh() def on_searchKeyword(self): keyword, ok = QInputDialog.getText(self, mlc.searchByKeyword, mlc.searchInputKeyword) if ok: try: result = getKeyword(keyword) except SwaggerException as e: errorDlg(self, e.message) return movies = result['films'] if len(movies) > 0: self.chooseMovies(mlc.searchResultByKeyword, movies) else: infoDlg(self, mlc.infoNotFound) def on_searchSequels(self): try: movies = getSequelsAndPrequels(self.selectedMovie.id()) except SwaggerException as e: errorDlg(self, e.message) return if len(movies) > 0: self.chooseMovies(mlc.searchResultSequels, movies) else: infoDlg(self, mlc.infoNotFound) def on_searchSimilar(self): try: result = getSimilar(self.selectedMovie.id()) except SwaggerException as e: errorDlg(self, e.message) return movies = result['items'] if len(movies) > 0: self.chooseMovies(mlc.searchResultSimilar, movies) else: infoDlg(self, mlc.infoNotFound)