Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package kawaii-player for openSUSE:Factory checked in at 2026-09-10 17:41:26 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/kawaii-player (Old) and /work/SRC/openSUSE:Factory/.kawaii-player.new.1265 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "kawaii-player" Thu Sep 10 17:41:26 2026 rev:14 rq:1376944 version:8.1.0.1 Changes: -------- --- /work/SRC/openSUSE:Factory/kawaii-player/kawaii-player.changes 2026-08-11 17:15:32.457991317 +0200 +++ /work/SRC/openSUSE:Factory/.kawaii-player.new.1265/kawaii-player.changes 2026-09-10 17:41:31.476034704 +0200 @@ -1,0 +2,17 @@ +Thu Sep 10 13:29:17 UTC 2026 - Daniel Donisa <[email protected]> + +- Fix url so osc service runall download_files works again + +------------------------------------------------------------------- +Wed Sep 9 13:53:50 UTC 2026 - Daniel Donisa <[email protected]> + +- Update to version 8.1.0-1: + * Fix Master navigation via web remote control + * fix showing total duration while transcoding + * reset episode-info also during series-info reset + * add episode info reset buttonn in series details page + * allow bulk episode renaming + * update extract_episode_number + * fix bulk-episode edits + +------------------------------------------------------------------- Old: ---- kawaii-player-8.0.0-1.tar.gz New: ---- kawaii-player-8.1.0-1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ kawaii-player.spec ++++++ --- /var/tmp/diff_new_pack.Ezu99c/_old 2026-09-10 17:41:32.951096432 +0200 +++ /var/tmp/diff_new_pack.Ezu99c/_new 2026-09-10 17:41:32.956096641 +0200 @@ -17,15 +17,15 @@ # See also http://en.opensuse.org/openSUSE:Specfile_guidelines -%define _over 8.0.0-1 +%define _over 8.1.0-1 %define _bver 8.0.0 Name: kawaii-player -Version: 8.0.0.1 +Version: 8.1.0.1 Release: 0 Summary: Multimedia player, library manager and media server License: GPL-3.0-or-later URL: https://github.com/kanishka-linux/kawaii-player -Source0: https://github.com/kanishka-linux/kawaii-player/archive/refs/tags/%{_over}.tar.gz#/%{name}-%{_over}.tar.gz +Source0: https://github.com/kanishka-linux/kawaii-player/archive/refs/tags/v%{_over}.tar.gz#/%{name}-%{_over}.tar.gz Source1: %{name}-rpmlintrc BuildRequires: fdupes BuildRequires: pkgconfig ++++++ kawaii-player-8.0.0-1.tar.gz -> kawaii-player-8.1.0-1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kawaii-player-8.0.0-1/kawaii_player/database.py new/kawaii-player-8.1.0-1/kawaii_player/database.py --- old/kawaii-player-8.0.0-1/kawaii_player/database.py 2026-02-23 19:36:06.000000000 +0100 +++ new/kawaii-player-8.1.0-1/kawaii_player/database.py 2026-03-08 01:07:35.000000000 +0100 @@ -777,24 +777,39 @@ def _extract_episode_number(self, ep_name: str) -> Optional[int]: if not ep_name: return None + + # Clean up: remove file extensions name = re.sub(r'\.\w{2,4}$', '', ep_name).lower().strip() + + # Remove brackets, parentheses, braces name = re.sub(r'\[.*?\]', '', name) name = re.sub(r'\(.*?\)', '', name) name = re.sub(r'\{.*?\}', '', name) name = name.strip() - for pattern in [ - r's\d+e(?P<num>\d+)', - r's\d+ep(?P<num>\d+)', - r'ep(?P<num>\d+)', - r'e(?P<num>\d+)', - r'episode\s*(?P<num>\d+)', - r'#(?P<num>\d+)', - r'(?<![a-z])(?<!\d)(?P<num>\d+)(?!\d)', - ]: + explicit_patterns = [ + r's\d+e(?P<num>\d+)', # S01E05 + r's\d+ep(?P<num>\d+)', # S01EP05 + r'ep(?P<num>\d+)', # EP01 + r'e(?P<num>\d+)', # E05 + r'episode\s*(?P<num>\d+)', # Episode 5 + ] + + for pattern in explicit_patterns: + m = re.search(pattern, name) + if m: + return int(m.group('num')) + + other_patterns = [ + r'#(?P<num>\d+)', # #01 + r'(?<![a-z])(?<!\d)(?P<num>\d+)(?!\d)', # standalone number + ] + + for pattern in other_patterns: m = re.search(pattern, name) if m: return int(m.group('num')) + return None def insert_episode_details(self, suggested_title: str, episode_details: list): @@ -931,12 +946,28 @@ """, (db_title, )) directory_path = cur.fetchone()[0] directory_name = os.path.split(directory_path)[-1] + + cur.execute(""" + select Path from Video where Title = ? + """, (db_title, )) + + paths = [row[0] for row in cur.fetchall()] + paths.sort() + + # restore default EP_NAMES and Ordering + for index, path in enumerate(paths): + ep_name = os.path.basename(path) + ep_name_sanitized = re.sub(r'-|_|\.', ' ', ep_name) + cur.execute(""" + update Video set EPN = ?, EP_NAME = ?, FileName = ? where Path = ? + """, (index, ep_name_sanitized, ep_name, path)) + + # restore Title to default title based on directory name cur.execute(""" update Video set Title = ? where directory = ? """, (directory_name, directory_path)) - self.logger.info(f"{cur.rowcount} -> updated for {directory_path}") cur.execute(""" @@ -972,6 +1003,71 @@ return success + def reset_only_series_episodes(self, db_title): + conn = sqlite3.connect(self.db_path) + cur = conn.cursor() + success = False + try: + cur.execute(""" + select Path from Video where Title = ? + """, (db_title, )) + + paths = [row[0] for row in cur.fetchall()] + paths.sort() + + # restore default EP_NAMES and Ordering + for index, path in enumerate(paths): + ep_name = os.path.basename(path) + ep_name_sanitized = re.sub(r'-|_|\.', ' ', ep_name) + cur.execute(""" + update Video set EPN = ?, EP_NAME = ?, FileName = ? where Path = ? + """, (index, ep_name_sanitized, ep_name, path)) + + conn.commit() + conn.close() + success = True + except Exception as err: + self.logger.error(f"error in resetting episode details: {db_title}, err: {str(err)}") + conn.rollback() + conn.close() + + return success + + def bulk_episode_edit(self, new_title, video_paths, episode_ordered_names): + success = True + updated_count = 0 + try: + with sqlite3.connect(self.db_path) as conn: + cur = conn.cursor() + if new_title and video_paths: + for path in video_paths: + cur.execute("UPDATE Video SET Title = ? WHERE Path = ?", (new_title, path)) + if cur.rowcount == 1: + self.logger.info(f"Title updated: {path} => {new_title}") + updated_count += 1 + + if episode_ordered_names and video_paths: + self.logger.info(f"Mode: Episode Numbers - Updating {len(video_paths)} files") + for path, episode_name in zip(video_paths, episode_ordered_names): + num = int(episode_name.replace('EP', '').strip()) - 1 + final_name = episode_name + if new_title: + final_name = f"{new_title} - {episode_name}" + + cur.execute(""" + UPDATE Video SET EP_NAME = ?, EPN = ?, FileName=? WHERE Path = ? + """, (final_name, num, final_name, path)) + + if cur.rowcount == 1: + self.logger.info(f"Title updated: {path} => {final_name} (EPN: {num})") + updated_count += 1 + + except Exception as err: + success = False + self.logger.error(f"Error: str{err}") + + return success + def get_series_count(self, filters: Dict[str, Any]) -> int: """Get total count of series matching filters""" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kawaii-player-8.0.0-1/kawaii_player/hls_transcoder.py new/kawaii-player-8.1.0-1/kawaii_player/hls_transcoder.py --- old/kawaii-player-8.0.0-1/kawaii_player/hls_transcoder.py 2026-02-23 19:36:06.000000000 +0100 +++ new/kawaii-player-8.1.0-1/kawaii_player/hls_transcoder.py 2026-03-08 01:07:35.000000000 +0100 @@ -102,6 +102,7 @@ playlist_path = os.path.join(cache_dir_path, 'playlist.m3u8') + duration = self._get_video_duration(video_path) # Check if already exists and completed if os.path.exists(playlist_path): finished_file = os.path.join(cache_dir_path, '.finished') @@ -110,7 +111,8 @@ 'success': True, 'url': f'/cache/{cache_dirname}/playlist.m3u8', 'status': 'completed', - 'progress': 100 + 'progress': 100, + 'duration': duration, } job_key = f"{video_path}_hls_{video_index}_{audio_index}" @@ -123,10 +125,10 @@ 'url': f'/cache/{cache_dirname}/playlist.m3u8', 'status': job.get('status', 'processing'), 'progress': job.get('progress', 0), + 'duration': duration, 'eta': job.get('eta', 'calculating...') } - duration = self._get_video_duration(video_path) settings = self._get_h264_settings() estimated_size = self._estimate_file_size(duration, settings['bitrate']) self.estimated_file_size[cache_dirname] = estimated_size @@ -154,7 +156,8 @@ 'status': 'processing', 'progress': 0, 'message': 'HLS transcoding started', - 'estimated_size': estimated_size + 'estimated_size': estimated_size, + 'duration': duration } def _transcode_worker(self, video_path, video_index, audio_index, output_dir, job_key, duration): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kawaii-player-8.0.0-1/kawaii_player/media_server.py new/kawaii-player-8.1.0-1/kawaii_player/media_server.py --- old/kawaii-player-8.0.0-1/kawaii_player/media_server.py 2026-02-23 19:36:06.000000000 +0100 +++ new/kawaii-player-8.1.0-1/kawaii_player/media_server.py 2026-03-08 01:07:35.000000000 +0100 @@ -295,7 +295,7 @@ dir_exact = None title_exact = None if "&db_title=" in srch: - dir_exact, title_exact = srch.split("&db_title=") + dir_exact, title_exact = urllib.parse.unquote(srch).split("&db_title=") for i, val in enumerate(ui.original_path_name): val = val.strip() @@ -1430,8 +1430,10 @@ if handle_auth_routes(self, 'GET', parsed_path.path, ui): return - if (self.path.startswith('/series-data') or - self.path.startswith('/series/')): + if self.path.startswith(( + '/series-data', '/series/', + '/cache/', '/api/player/' + )): if require_admin_auth(self, ui): return @@ -1526,6 +1528,9 @@ result['file_size'] ) #self.send_raw_response(result['data'], result['content_type'], 200) + elif self.path.startswith("/api/player/"): + path = self.path.split('/api/player/', 1)[-1] + self.get_the_content(path, 0) else: self.do_init_function(type_request='get') @@ -2621,9 +2626,11 @@ if handle_auth_routes(self, 'POST', parsed_path.path, ui): return - if (self.path in auth_required_routes and - require_admin_auth(self, ui)): - return + if (self.path in auth_required_routes or + self.path.startswith(('/series/', '/api/player/')) + ): + if require_admin_auth(self, ui): + return if self.path == '/admin/series-update': self.handle_series_update() @@ -2681,6 +2688,15 @@ user_agent = self.headers.get('User-Agent', '') data, status_code = ui.track_extractor.handle_transcode_cancel_request(series_id, request_body, user_agent) self.send_json_response(data, status_code) + elif self.path.startswith('/api/player/sending_web_command'): + content = self.rfile.read(int(self.headers['Content-Length'])) + if isinstance(content, bytes): + content = str(content, 'utf-8') + content = json.loads(content) + param = "param={}".format(content.get("param")) + value = "widget={}".format(content.get("widget")) + self.final_message(bytes('Command Recieved', 'utf-8')) + ui.gui_signals.player_command(param.replace('+', ' '), value.replace('+', ' ')) else: self.do_init_function(type_request='post') @@ -2962,6 +2978,16 @@ result = {'success': False, 'error': 'Not Found'} else: result = {'success': False, 'error': 'db_title needed'} + elif action == 'reset_only_series_episodes': + db_title = data.get("db_title", "") + if db_title: + res = ui.media_data.reset_only_series_episodes(db_title) + if res: + result = {'success': True, 'message': f"{db_title} episodes reset successfully"} + else: + result = {'success': False, 'error': 'Not Found'} + else: + result = {'success': False, 'error': 'db_title needed'} else: result = {'success': False, 'error': 'Invalid action'} @@ -3369,56 +3395,44 @@ return {'success': False, 'error': f"Database update error: {str(e)}"} def update_multiple_paths(self, data): - """Update title for multiple video paths""" - global home, logger + global logger, ui - conn = sqlite3.connect(os.path.join(home, 'VideoDB', 'Video.db')) + video_paths = data.get('video_paths', []) + new_title = data.get('new_title', '').strip() + episode_ordered_names = data.get('episode_ordered_names', []) + + if not isinstance(video_paths, list) or not video_paths: + return {'success': False, 'error': 'Video paths required'} + + if not new_title and not episode_ordered_names: + return {'success': False, 'error': 'Either new_title or episode_ordered_names required'} + + if episode_ordered_names and len(episode_ordered_names) != len(video_paths): + return {'success': False, 'error': f'Mismatch: {len(video_paths)} paths vs {len(episode_ordered_names)} names'} + + if new_title and not episode_ordered_names: + mode_msg = f"Title Only -> '{new_title}'" + elif episode_ordered_names and not new_title: + mode_msg = "Episode Numbers Only" + else: + mode_msg = "Both Title & Numbers" + + logger.info(f"Mode: {mode_msg} - Updating {len(video_paths)} files") + + # Call DB method try: - cur = conn.cursor() - - # Handle video_paths - now expecting array directly - video_paths = data.get('video_paths', []) - - # Ensure it's a list - if not isinstance(video_paths, list): - logger.error(f"Warning: video_paths is not a list: {type(video_paths)} - {repr(video_paths)}") - video_paths = [] - - new_title = data.get('new_title', '').strip() - - logger.info(f"Updating multiple paths: {len(video_paths)} files -> '{new_title}'") - logger.info(f"Video paths: {video_paths}") - - if not video_paths or not new_title: - conn.close() - return {'success': False, 'error': 'Video paths and new title are required'} - - updated_count = 0 - for path in video_paths: - cur.execute("update Video set Title = ? where Path = ?", (new_title, path)) - if cur.rowcount == 1: - logger.info(f"title successfully updated {path} => {new_title}") - updated_count += 1 - else: - ui.logger.info(f"title successfully updated {path} => {new_title}") - - if updated_count != len(video_paths): - raise "mismatch in updated count: video_path_count = {len(video_path)}, updated_count = {updated_count}" - - conn.commit() - conn.close() + success = ui.media_data.bulk_episode_edit(new_title, video_paths, episode_ordered_names) + if not success: + return {'success': False, 'error': 'Database operation failed'} return { 'success': True, - 'message': f"Successfully updated {updated_count} episodes to '{new_title}'" + 'message': f"Successfully updated {len(video_paths)} episodes ({mode_msg})", + 'updated_count': len(video_paths) } - + except Exception as e: - logger.error(f"Error in update_multiple_paths: {e}") - import traceback - traceback.print_exc() - conn.rollback() - conn.close() + logger.error(f"Error: {str(e)}") return {'success': False, 'error': f"Database error: {str(e)}"} def serve_hls_playlist(self, cache_dirname): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kawaii-player-8.0.0-1/kawaii_player/web/admin.css new/kawaii-player-8.1.0-1/kawaii_player/web/admin.css --- old/kawaii-player-8.0.0-1/kawaii_player/web/admin.css 2026-02-23 19:36:06.000000000 +0100 +++ new/kawaii-player-8.1.0-1/kawaii_player/web/admin.css 2026-03-08 01:07:35.000000000 +0100 @@ -2805,3 +2805,191 @@ font-size: 1rem; } } + +/* Edit Multiple Episodes - Compact Styles */ + +.edit-mode-option { + padding: 15px; + border: 1px solid #e0e0e0; + border-radius: 6px; + background: #fafafa; + margin-bottom: 15px; +} + +.edit-mode-radio-group { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 10px; +} + +.edit-mode-radio-group input[type="radio"] { + cursor: pointer; + width: 18px; + height: 18px; + margin: 0; + accent-color: #007bff; +} + +.edit-mode-radio-group input[type="radio"]:focus { + outline: none; +} + +.edit-mode-radio-group label { + cursor: pointer; + margin: 0; + font-size: 14px; +} + +.edit-mode-content { + animation: slideDown 0.3s ease; +} + +@keyframes slideDown { + from { opacity: 0; max-height: 0; overflow: hidden; } + to { opacity: 1; max-height: 500px; overflow: visible; } +} + +#edit-mode-preview-mappings-content { + display: none; + width: 100%; + margin-top: 15px; + border-collapse: collapse; + font-size: 13px; +} + +#edit-mode-preview-mappings-content thead { + background: #f5f5f5; + border-bottom: 2px solid #ddd; +} + +#edit-mode-preview-mappings-content th { + padding: 10px; + text-align: left; + font-weight: 600; + color: #333; +} + +#edit-mode-preview-mappings-content tbody tr { + border-bottom: 1px solid #eee; + transition: background-color 0.2s ease; +} + +#edit-mode-preview-mappings-content tbody tr:hover { + background: #f9f9f9; +} + +#edit-mode-preview-mappings-content td { + padding: 8px 10px; + word-break: break-all; + color: #555; +} + +#edit-mode-preview-mappings-content .edit-mode-file-path { + font-family: 'Courier New', monospace; + font-size: 12px; + color: #666; + background: #f9f9f9; + border-right: 1px solid #eee; + max-width: 60%; + word-break: break-word; +} + +#edit-mode-preview-mappings-content .edit-mode-preview-result { + color: #007bff; + font-weight: 500; +} + +#edit-mode-preview-section.edit-mode-expanded { + border: 1px solid #e0e0e0; + padding: 15px; + border-radius: 6px; + background: #f5f9ff; +} + +.edit-mode-preview-toggle-btn { + background: none; + border: none; + cursor: pointer; + font-size: 14px; + padding: 10px 0; + color: #007bff; + text-align: left; + width: 100%; + font-weight: 500; + transition: color 0.2s ease; + outline: none; +} + +.edit-mode-preview-toggle-btn:hover { + color: #0056b3; +} + +.edit-mode-preview-toggle-btn:focus { + outline: none; +} + +.edit-mode-content .field-help { + font-size: 12px; + color: #999; + margin-top: 5px; + font-style: italic; +} + +/* Episode List Styles */ +.episode-list { + max-height: 300px; + overflow-y: auto; + border: 1px solid #e0e0e0; + border-radius: 4px; + background: #fff; +} + +.episode-item { + padding: 10px 15px; + border-bottom: 1px solid #f0f0f0; + display: flex; + align-items: center; + gap: 10px; +} + +.episode-item:last-child { + border-bottom: none; +} + +.episode-details { + flex: 1; + min-width: 0; +} + +.episode-path { + font-family: 'Courier New', monospace; + font-size: 12px; + color: #666; + word-break: break-all; + white-space: pre-wrap; +} + +@media (max-width: 768px) { + .edit-mode-option { padding: 12px; margin-bottom: 12px; } + .edit-mode-radio-group { gap: 8px; flex-wrap: wrap; } + .edit-mode-radio-group label { font-size: 13px; } + #edit-mode-preview-mappings-content { font-size: 12px; } + #edit-mode-preview-mappings-content th, + #edit-mode-preview-mappings-content td { padding: 6px 8px; } + .episode-list { max-height: 200px; } +} + +@media (prefers-color-scheme: dark) { + .edit-mode-option { background: #2a2a2a; border-color: #444; } + #edit-mode-preview-section.edit-mode-expanded { background: #1a2a3a; border-color: #444; } + #edit-mode-preview-mappings-content thead { background: #333; border-color: #555; } + #edit-mode-preview-mappings-content tbody tr { border-color: #444; } + #edit-mode-preview-mappings-content tbody tr:hover { background: #2a2a2a; } + #edit-mode-preview-mappings-content td { color: #ccc; } + .edit-mode-preview-toggle-btn { color: #66b3ff; } + .edit-mode-preview-toggle-btn:hover { color: #99ccff; } + .episode-list { border-color: #444; background: #2a2a2a; } + .episode-item { border-bottom-color: #3a3a3a; } + .episode-path { color: #aaa; } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kawaii-player-8.0.0-1/kawaii_player/web/admin.js new/kawaii-player-8.1.0-1/kawaii_player/web/admin.js --- old/kawaii-player-8.0.0-1/kawaii_player/web/admin.js 2026-02-23 19:36:06.000000000 +0100 +++ new/kawaii-player-8.1.0-1/kawaii_player/web/admin.js 2026-03-08 01:07:35.000000000 +0100 @@ -2081,60 +2081,213 @@ this.openMultiplePathsEditModal(selectedPaths); } - // FIXED: Remove escapeHtml from JSON string + generateNumberedTitles(count, startNumber = 1) { + const titles = []; + for (let i = 0; i < count; i++) { + const episodeNum = startNumber + i; + const formatted = `EP${episodeNum.toString().padStart(2, '0')}`; + titles.push(formatted); + } + return titles; + } + + updatePreviewMappings() { + const mode = document.querySelector('input[name="edit-mode-rename-mode"]:checked').value; + const previewContainer = document.getElementById('edit-mode-preview-mappings-content'); + if (!previewContainer) return; + + let rows = ''; + const paths = this.currentEditPaths || []; + + if (mode === 'both') { + const baseTitle = document.getElementById('edit-mode-both-title-input').value.trim() || '[Enter title]'; + const startNum = parseInt(document.getElementById('edit-mode-both-numbers-start').value) || 1; + const numbers = this.generateNumberedTitles(paths.length, startNum); + paths.forEach((path, i) => { + const combined = `${baseTitle} - ${numbers[i]}`; + rows += `<tr><td class="edit-mode-file-path">${this.escapeHtml(path)}</td><td class="edit-mode-preview-result">${this.escapeHtml(combined)}</td></tr>`; + }); + } + + previewContainer.innerHTML = rows || '<tr><td colspan="2" style="text-align:center;color:#999;">No preview</td></tr>'; + } + + // togglePreviewSection() + togglePreviewSection() { + const btn = document.getElementById('edit-mode-preview-toggle-btn'); + const content = document.getElementById('edit-mode-preview-mappings-content'); + const section = document.getElementById('edit-mode-preview-section'); + + if (!btn || !content || !section) return; + + const isExpanded = section.classList.contains('edit-mode-expanded'); + if (isExpanded) { + section.classList.remove('edit-mode-expanded'); + btn.textContent = '▶ Preview'; + content.style.display = 'none'; + } else { + section.classList.add('edit-mode-expanded'); + btn.textContent = '▼ Preview'; + content.style.display = 'table'; + this.updatePreviewMappings(); + } + } + + // onModeChange() + onModeChange() { + const mode = document.querySelector('input[name="edit-mode-rename-mode"]:checked').value; + document.getElementById('edit-mode-title-only-section').style.display = mode === 'title-only' ? 'block' : 'none'; + document.getElementById('edit-mode-both-section').style.display = mode === 'both' ? 'block' : 'none'; + previewSection.style.display = mode === 'both' ? 'block' : 'none'; + this.updatePreviewMappings(); + } + + // submitMultiplePathsEdit() + async submitMultiplePathsEdit(event) { + event.preventDefault(); + + const videoPaths = this.currentEditPaths || []; + if (videoPaths.length === 0) { + this.showErrorMessage("No episodes selected"); + return; + } + + const mode = document.querySelector('input[name="edit-mode-rename-mode"]:checked').value; + const formData = { action: 'update_multiple_paths', video_paths: videoPaths }; + + // Mode 1: Title Only + if (mode === 'title-only') { + const newTitle = document.getElementById('edit-mode-title-only-input').value.trim(); + if (!newTitle) { this.showErrorMessage('Enter a title'); return; } + formData.new_title = newTitle; + } + // Mode 2: Both Title & Numbers + else if (mode === 'both') { + const baseTitle = document.getElementById('edit-mode-both-title-input').value.trim(); + if (!baseTitle) { this.showErrorMessage('Enter a series title'); return; } + const startNum = parseInt(document.getElementById('edit-mode-both-numbers-start').value) || 1; + formData.new_title = baseTitle; + formData.episode_ordered_names = this.generateNumberedTitles(videoPaths.length, startNum); + } + + try { + this.showUpdateProgress('Updating...'); + const response = await fetch('/admin/series-update', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(formData) + }); + + if (!response.ok) throw new Error('Update failed'); + const result = await response.json(); + + if (result.success) { + this.currentEditPaths = null; + this.closeEditModal(); + await this.loadTitles(); + this.showSuccessMessage(result.message); + this.selectedEpisodes.clear(); + this.updateSelectionUI(); + if (this.currentTitle) { + await this.showTitleDetails(this.currentTitle.directory_hash, this.currentTitle.title); + } + } else { + throw new Error(result.error); + } + } catch (error) { + this.currentEditPaths = null; + this.showErrorMessage('Update failed: ' + error.message); + } + } + openMultiplePathsEditModal(selectedPaths) { const pathsToSend = selectedPaths || []; - console.log("Paths to send in modal:", pathsToSend); // Debug log + console.log("Paths to send in modal:", pathsToSend); this.currentEditPaths = pathsToSend; - + + // Build episode list with proper escaping + const episodeListHTML = pathsToSend.map(path => ` + <div class="episode-item"> + <div class="episode-path">${this.escapeHtml(path)}</div> + </div> + `).join(''); + const modalHTML = ` <div id="edit-modal" class="details-panel active"> <div class="details-header"> - <h3>Edit Multiple Episodes</h3> + <h3>Edit Multiple Episodes (${pathsToSend.length})</h3> <button class="details-close" onclick="admin.closeEditModal()">×</button> </div> <div class="details-content"> - <div class="edit-info"> - Editing ${pathsToSend.length} episode(s). Only the title will be updated. - </div> - - <form id="edit-form" onsubmit="admin.submitMultiplePathsEdit(event)"> + <form id="edit-mode-form" onsubmit="admin.submitMultiplePathsEdit(event)"> - <div class="details-section"> - <h4>Episode Information</h4> - <div class="series-field"> - <span class="field-label">New Title:</span> - <input type="text" id="paths-new-title" placeholder="Enter new title for all selected episodes" required class="field-input"> + <!-- Mode 1: Title Only --> + <div class="edit-mode-option"> + <div class="edit-mode-radio-group"> + <input type="radio" id="edit-mode-title" name="edit-mode-rename-mode" value="title-only" checked onchange="admin.onModeChange()"> + <label for="edit-mode-title"><strong>Title Only</strong></label> + </div> + <div class="field-help">All episodes will get the same title</div> + <div id="edit-mode-title-only-section" class="edit-mode-content"> + <input type="text" id="edit-mode-title-only-input" placeholder="Enter new title" onkeyup="admin.updatePreviewMappings()" class="field-input"> </div> </div> - - <div class="details-section"> + + <!-- Mode 2: Title + Episode Numbers --> + <div class="edit-mode-option"> + <div class="edit-mode-radio-group"> + <input type="radio" id="edit-mode-both" name="edit-mode-rename-mode" value="both" onchange="admin.onModeChange()"> + <label for="edit-mode-both"><strong>Title + Episode Numbers</strong></label> + </div> + <div class="field-help">Each episode will be numbered in order (Series - EP01, Series - EP02...)</div> + <div id="edit-mode-both-section" class="edit-mode-content" style="display:none;"> + <div style="margin-left:30px;margin-top:10px;"> + <span class="field-label">Series Title:</span> + <input type="text" id="edit-mode-both-title-input" placeholder="Series name" onkeyup="admin.updatePreviewMappings()" class="field-input"> + </div> + <div style="margin-left:30px;margin-top:10px;"> + <span class="field-label">Starting from:</span> + <input type="number" id="edit-mode-both-numbers-start" value="1" min="1" onchange="admin.updatePreviewMappings()" class="field-input" style="max-width:100px;"> + <div class="field-help">Creates: Series - EP01, Series - EP02...</div> + </div> + </div> + </div> + + <!-- Preview Section --> + <div class="details-section" id="edit-mode-preview-section" style="margin-top:20px;"> + <button type="button" id="edit-mode-preview-toggle-btn" class="edit-mode-preview-toggle-btn" onclick="admin.togglePreviewSection()"> + ▶ Preview + </button> + <table id="edit-mode-preview-mappings-content"> + <thead><tr><th>File Path</th><th>New Name</th></tr></thead> + <tbody></tbody> + </table> + </div> + + <!-- Selected Episodes List --> + <div class="details-section" style="margin-top:20px;"> <h4>Selected Episodes (${pathsToSend.length})</h4> <div class="episode-list"> - ${pathsToSend.map(path => ` - <div class="episode-item"> - <div class="episode-details"> - <div class="episode-path">${this.escapeHtml(path)}</div> - </div> - </div> - `).join('')} + ${episodeListHTML} </div> </div> - + + <!-- Action Buttons --> <div class="form-actions-details"> <button type="button" class="btn btn-secondary" onclick="admin.closeEditModal()">Cancel</button> - <button type="submit" class="btn btn-primary">Update All</button> + <button type="submit" class="btn btn-primary">Update Episodes</button> </div> </form> </div> </div> `; - + document.body.insertAdjacentHTML('beforeend', modalHTML); document.body.style.overflow = 'hidden'; + setTimeout(() => { this.updatePreviewMappings(); }, 100); } @@ -3069,66 +3222,6 @@ this.showErrorMessage('Update failed: ' + error.message); } } - - // Type 3: Submit multiple paths edit - async submitMultiplePathsEdit(event) { - event.preventDefault(); - - const videoPaths = this.currentEditPaths || []; - console.log("Using stored paths:", videoPaths.length, "episodes"); // Debug log - - if (videoPaths.length === 0) { - this.showErrorMessage("No episodes selected for editing."); - this.currentEditPaths = null; // Reset on error - return; - } - - const formData = { - action: 'update_multiple_paths', - video_paths: videoPaths, - new_title: document.getElementById('paths-new-title').value.trim() - }; - - console.log("Multiple paths form data to send:", formData.video_paths.length, "paths"); // Debug log - - try { - this.showUpdateProgress('Updating multiple episodes...'); - - const response = await fetch('/admin/series-update', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(formData) - }); - - if (!response.ok) { - throw new Error('Update failed'); - } - - const result = await response.json(); - - if (result.success) { - // SUCCESS: Reset everything - this.currentEditPaths = null; - this.closeEditModal(); - await this.loadTitles(); - this.showSuccessMessage(result.message || 'Episodes updated successfully'); - this.selectedEpisodes.clear(); - this.updateSelectionUI(); - if (this.currentTitle) { - await this.showTitleDetails(this.currentTitle.directory_hash, this.currentTitle.title); - } - } else { - throw new Error(result.error || 'Update failed'); - } - - } catch (error) { - // ERROR: Reset and show error - this.currentEditPaths = null; - this.showErrorMessage('Update failed: ' + error.message); - } - } // NEW: Movie metadata methods fetchMovieMetadata() { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kawaii-player-8.0.0-1/kawaii_player/web/player_control.js new/kawaii-player-8.1.0-1/kawaii_player/web/player_control.js --- old/kawaii-player-8.0.0-1/kawaii_player/web/player_control.js 2026-02-23 19:36:06.000000000 +0100 +++ new/kawaii-player-8.1.0-1/kawaii_player/web/player_control.js 2026-03-08 01:07:35.000000000 +0100 @@ -6,7 +6,8 @@ const CONFIG = { BASE_URL: window.location.origin, SYNC_INTERVAL: 1000, // 1 second for master/slave mode - AUTO_HIDE_ALERT: 2000 // 2 seconds + AUTO_HIDE_ALERT: 2000, // 2 seconds + BASE_URL_PLAYER: `${window.location.origin}/api/player` }; // =========================== @@ -21,6 +22,7 @@ playing: false, currentTime: 0, duration: 0, + initialDuration: 0, volume: 75, syncInterval: null, previousIndex: -1, @@ -176,7 +178,7 @@ */ async function getRemoteControlStatus() { try { - const response = await fetch(`${CONFIG.BASE_URL}/get_remote_control_status`); + const response = await fetch(`${CONFIG.BASE_URL_PLAYER}/get_remote_control_status`); if (!response.ok) throw new Error('Failed to get remote status'); const text = await response.text(); @@ -202,7 +204,7 @@ */ async function sendRemoteCommand(endpoint) { try { - const response = await fetch(`${CONFIG.BASE_URL}${endpoint}`); + const response = await fetch(`${CONFIG.BASE_URL_PLAYER}${endpoint}`); return response.ok; } catch (error) { console.error('Error sending remote command:', error); @@ -216,7 +218,7 @@ */ async function sendPostCommand(endpoint, data) { try { - const response = await fetch(`${CONFIG.BASE_URL}${endpoint}`, { + const response = await fetch(`${CONFIG.BASE_URL_PLAYER}${endpoint}`, { method: 'POST', headers: { 'Content-Type': 'application/json' @@ -300,7 +302,7 @@ const masterSlaveControls = document.querySelectorAll('.master-slave-only'); if (newMode === 'in-browser') { - const response = await fetch(`${CONFIG.BASE_URL}/remote_off.htm`); + const response = await fetch(`${CONFIG.BASE_URL_PLAYER}/remote_off.htm`); videoPlayerContainer.classList.remove('hidden'); thumbnailArea.classList.add('hidden'); connectionStatus.classList.add('hidden'); @@ -310,7 +312,7 @@ stopRemoteSync(); initBrowserPlayer(); } else { - const response = await fetch(`${CONFIG.BASE_URL}/remote_on.htm`); + const response = await fetch(`${CONFIG.BASE_URL_PLAYER}/remote_on.htm`); videoPlayerContainer.classList.add('hidden'); thumbnailArea.classList.remove('hidden'); connectionStatus.classList.remove('hidden'); @@ -321,10 +323,10 @@ startRemoteSync(); const playlistUrl = `/site=video&opt=available&s=${state.seriesInfo.dir_hash}&db_title=${state.seriesInfo.db_title}&exact.m3u`; - await fetch(`${CONFIG.BASE_URL}${playlistUrl}`); + await fetch(`${CONFIG.BASE_URL_PLAYER}${playlistUrl}`); console.log('Playlist loaded:', playlistUrl); - const response1 = await fetch(`${CONFIG.BASE_URL}/toggle_master_slave`); + const response1 = await fetch(`${CONFIG.BASE_URL_PLAYER}/toggle_master_slave`); const text = await response1.text(); const actualMode = text.toLowerCase().trim(); // Update status text based on mode @@ -335,7 +337,7 @@ statusText.textContent = '📺 Desktop Player Connected (Slave Mode)'; } else { console.log('📺 Mismatch', newMode, actualMode, "toggling again"); - const response2 = await fetch(`${CONFIG.BASE_URL}/toggle_master_slave`); + const response2 = await fetch(`${CONFIG.BASE_URL_PLAYER}/toggle_master_slave`); const text2 = await response2.text(); const actualMode2 = text2.toLowerCase().trim(); statusText.textContent = `🖥️ Desktop Player Connected (${capitalizeFirstLetter(actualMode2)} Mode)`; @@ -369,7 +371,11 @@ // Add event listeners videoPlayer.addEventListener('timeupdate', updateProgress); videoPlayer.addEventListener('loadedmetadata', () => { - state.duration = videoPlayer.duration; + if (videoPlayer.duration <= 0) { + state.duration = state.initialDuration; + } else { + state.duration = videoPlayer.duration; + } document.getElementById('totalTime').textContent = formatTime(state.duration); }); videoPlayer.addEventListener('play', () => { @@ -460,13 +466,25 @@ if (!videoPlayer) return; state.currentTime = videoPlayer.currentTime; - state.duration = videoPlayer.duration; - + + // Prefer manually set initialDuration (from transcode API response) + if (state.initialDuration > 0) { + state.duration = state.initialDuration; + } else if (isFinite(videoPlayer.duration) && videoPlayer.duration > 0) { + state.duration = videoPlayer.duration; + } + const progressFill = document.getElementById('progressFill'); const currentTimeEl = document.getElementById('currentTime'); + const totalTime = document.getElementById('totalTime'); - const percentage = (state.currentTime / state.duration) * 100; - progressFill.style.width = `${percentage}%`; + if (state.duration > 0) { + const percentage = (state.currentTime / state.duration) * 100; + progressFill.style.width = `${percentage}%`; + totalTime.textContent = formatTime(state.duration); + console.log(totalTime.textContent, "what..", state.initialDuration) + } + currentTimeEl.textContent = formatTime(state.currentTime); } @@ -1343,6 +1361,7 @@ if (data.duration) { state.duration = data.duration; + state.initialDuration = data.duration; document.getElementById('totalTime').textContent = formatTime(data.duration); } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kawaii-player-8.0.0-1/kawaii_player/web/series_details.js new/kawaii-player-8.1.0-1/kawaii_player/web/series_details.js --- old/kawaii-player-8.0.0-1/kawaii_player/web/series_details.js 2026-02-23 19:36:06.000000000 +0100 +++ new/kawaii-player-8.1.0-1/kawaii_player/web/series_details.js 2026-03-08 01:07:35.000000000 +0100 @@ -229,6 +229,10 @@ <span class="fetch-icon">🎨</span> <span class="fetch-text">Fetch Fanart</span> </button> + <button class="fetch-btn fetch-reset-episodes-btn" id="fetch-reset-episodes-btn" title="Reset episodes"> + <span class="fetch-icon">🗑️</span> + <span class="fetch-text">Reset Episodes</span> + </button> </div> `; } @@ -236,6 +240,7 @@ setupFetchMediaHandlers() { const metadataBtn = document.getElementById('fetch-metadata-btn'); const fanartBtn = document.getElementById('fetch-fanart-btn'); + const resetEpisodesBtn = document.getElementById('fetch-reset-episodes-btn'); if (metadataBtn) { metadataBtn.addEventListener('click', () => { @@ -248,6 +253,95 @@ this.showFetchMediaDialog('fanart'); }); } + + if (resetEpisodesBtn) { + resetEpisodesBtn.addEventListener('click', () => { + this.showResetEpisodesDialog(); + }); + } + } + + showResetEpisodesDialog() { + const title = this.seriesData?.series_info?.db_title; + + const modalHtml = ` + <div class="fetch-modal-overlay active" id="reset-episodes-overlay"> + <div class="fetch-modal-dialog"> + <div class="fetch-modal-title">🗑️ Reset Episodes</div> + <div class="fetch-modal-subtitle">This will reset all episodes for <strong>${this.escapeHtml(title)}</strong>. This action cannot be undone.</div> + <div class="fetch-modal-buttons"> + <button class="fetch-modal-btn fetch-modal-btn-cancel" id="reset-episodes-cancel-btn">Cancel</button> + <button class="fetch-modal-btn fetch-modal-btn-confirm" id="reset-episodes-confirm-btn" style="background:#e53935;">Reset Episodes</button> + </div> + </div> + </div> + `; + + const modalContainer = document.createElement('div'); + modalContainer.id = 'reset-episodes-container'; + modalContainer.innerHTML = modalHtml; + document.body.appendChild(modalContainer); + + const overlay = document.getElementById('reset-episodes-overlay'); + const confirmBtn = document.getElementById('reset-episodes-confirm-btn'); + const cancelBtn = document.getElementById('reset-episodes-cancel-btn'); + + const closeModal = () => { + overlay.classList.remove('active'); + setTimeout(() => { + const container = document.getElementById('reset-episodes-container'); + if (container?.parentNode) container.parentNode.removeChild(container); + }, 200); + }; + + cancelBtn.addEventListener('click', closeModal); + overlay.addEventListener('click', (e) => { if (e.target === overlay) closeModal(); }); + + const escapeHandler = (e) => { + if (e.key === 'Escape') { closeModal(); document.removeEventListener('keydown', escapeHandler); } + }; + document.addEventListener('keydown', escapeHandler); + + confirmBtn.addEventListener('click', async () => { + confirmBtn.disabled = true; + cancelBtn.disabled = true; + confirmBtn.innerHTML = '<span class="fetch-loading-spinner"></span>Resetting...'; + + try { + await this.resetSeriesEpisodes(title); + closeModal(); + this.showMessage('Success', 'Episodes reset successfully!', 'success'); + setTimeout(() => location.reload(), 1500); + } catch (error) { + console.error('Reset episodes error:', error); + this.showMessage('Error', error.message || 'Failed to reset episodes', 'error'); + confirmBtn.disabled = false; + cancelBtn.disabled = false; + confirmBtn.innerHTML = 'Reset Episodes'; + } + }); + } + + async resetSeriesEpisodes(dbTitle) { + const response = await fetch('/admin/series-update', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'same-origin', + body: JSON.stringify({ + action: 'reset_only_series_episodes', + db_title: dbTitle, + reason: 'reset_entry' + }) + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.error || `HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + if (!result.success) throw new Error(result.error || 'Reset episodes failed'); + return result; } showFetchMetadataDialog() { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kawaii-player-8.0.0-1/kawaii_player/webm_transcoder.py new/kawaii-player-8.1.0-1/kawaii_player/webm_transcoder.py --- old/kawaii-player-8.0.0-1/kawaii_player/webm_transcoder.py 2026-02-23 19:36:06.000000000 +0100 +++ new/kawaii-player-8.1.0-1/kawaii_player/webm_transcoder.py 2026-03-08 01:07:35.000000000 +0100 @@ -97,6 +97,7 @@ cache_filename = self._generate_cache_key(video_path, video_index, audio_index) cache_path = os.path.join(self.cache_dir, cache_filename) + duration = self._get_video_duration(video_path) # Check if already exists if os.path.exists(cache_path): file_size = os.path.getsize(cache_path) @@ -106,7 +107,8 @@ 'url': f'/cache/{cache_filename}', 'status': 'completed', 'progress': 100, - 'size': file_size + 'size': file_size, + 'duration': duration } job_key = f"{video_path}_webm_{video_index}_{audio_index}" @@ -119,10 +121,10 @@ 'url': f'/cache/{cache_filename}', 'status': job.get('status', 'processing'), 'progress': job.get('progress', 0), - 'eta': job.get('eta', 'calculating...') + 'eta': job.get('eta', 'calculating...'), + 'duration': duration } - duration = self._get_video_duration(video_path) settings = self._get_vp8_settings() estimated_size = self._estimate_file_size(duration, settings['bitrate']) self.estimated_file_size[cache_filename] = estimated_size @@ -150,7 +152,8 @@ 'status': 'processing', 'progress': 0, 'message': 'WebM transcoding started', - 'estimated_size': estimated_size + 'estimated_size': estimated_size, + 'duration': duration } def _transcode_worker(self, video_path, video_index, audio_index, output_path, job_key, duration):
