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()">&times;</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):

Reply via email to