Skip to content

Commit 998f7ae

Browse files
committed
feat: added audio normalization flag for ffmpeg/encoding
style: Made window buttons smaller. removed rounding. fix: Implemented fixes for other subtitle providers. Still can be finicky. SubDL doesn't like my API keys. Other web scrappers don't work too well.
1 parent 1687422 commit 998f7ae

10 files changed

Lines changed: 199 additions & 33 deletions

File tree

EncodeForge/src/main/java/com/encodeforge/MainApp.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,15 +85,20 @@ public void init() throws Exception {
8585
}
8686

8787
// Check if dependencies are installed
88+
// ALWAYS check on every startup to detect newly-added requirements (e.g., bs4, lxml)
8889
Map<String, Boolean> libStatus = dependencyManager.checkRequiredLibraries().get();
8990
boolean allLibsInstalled = libStatus.values().stream().allMatch(v -> v);
9091
boolean ffmpegInstalled = dependencyManager.checkFFmpeg().get();
9192

9293
logger.info("Dependency status:");
9394
logger.info(" Python libraries: {}", allLibsInstalled ? "✓" : "✗");
95+
libStatus.forEach((lib, installed) ->
96+
logger.info(" - {}: {}", lib, installed ? "✓" : "✗"));
9497
logger.info(" FFmpeg: {}", ffmpegInstalled ? "✓" : "✗");
9598

9699
// If anything is missing, show initialization dialog on startup
100+
// This ensures new libraries (like bs4/lxml) get installed even if
101+
// the app was previously initialized
97102
if (!allLibsInstalled || !ffmpegInstalled) {
98103
logger.info("Some dependencies missing, will show initialization dialog");
99104
pendingDependencyInstall = true;

EncodeForge/src/main/java/com/encodeforge/controller/MainController.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ private enum ResizeDirection {
170170
@FXML private ComboBox<String> quickQualityCombo;
171171
@FXML private ComboBox<String> quickPresetCombo;
172172
@FXML private CheckBox quickHwAccelCheck;
173+
@FXML private CheckBox quickAudioNormalizeCheck;
173174
@FXML private CheckBox quickDownloadSubsCheck;
174175
@FXML private CheckBox quickRenameCheck;
175176
@FXML private Button configSubtitlesButton;
@@ -3101,6 +3102,10 @@ private void updateSettingsFromQuickUI() {
31013102
settings.setHardwareDecoding(quickHwAccelCheck.isSelected());
31023103
}
31033104

3105+
if (quickAudioNormalizeCheck != null) {
3106+
settings.setAudioNormalize(quickAudioNormalizeCheck.isSelected());
3107+
}
3108+
31043109
if (quickDownloadSubsCheck != null) {
31053110
settings.setDownloadSubtitles(quickDownloadSubsCheck.isSelected());
31063111
}
@@ -3716,6 +3721,11 @@ private void setupQuickSettings() {
37163721
logger.debug("Loaded saved hardware acceleration: {}", settings.isHardwareDecoding());
37173722
}
37183723

3724+
if (quickAudioNormalizeCheck != null) {
3725+
quickAudioNormalizeCheck.setSelected(settings.isAudioNormalize());
3726+
logger.debug("Loaded saved audio normalize: {}", settings.isAudioNormalize());
3727+
}
3728+
37193729
if (quickDownloadSubsCheck != null) {
37203730
quickDownloadSubsCheck.setSelected(false);
37213731
}

EncodeForge/src/main/java/com/encodeforge/controller/SettingsController.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ public class SettingsController {
9898
@FXML private RadioButton audioLanguageRadio;
9999
@FXML private TextField audioLanguageField;
100100
@FXML private ComboBox<String> audioBitrateCombo;
101+
@FXML private CheckBox audioNormalizeCheck;
101102

102103
// Subtitle Settings
103104
@FXML private CheckBox convertSubsCheck;
@@ -397,6 +398,7 @@ private void loadSettings() {
397398
crfSpinner.getValueFactory().setValue(settings.getNvencCq());
398399

399400
audioCodecCombo.setValue(settings.getAudioCodec());
401+
audioNormalizeCheck.setSelected(settings.isAudioNormalize());
400402

401403
convertSubsCheck.setSelected(settings.isConvertSubtitles());
402404
subtitleFormatCombo.setValue(settings.getSubtitleFormat());
@@ -458,6 +460,7 @@ private void saveSettings() {
458460
settings.setNvencCq(crfSpinner.getValue());
459461

460462
settings.setAudioCodec(audioCodecCombo.getValue());
463+
settings.setAudioNormalize(audioNormalizeCheck.isSelected());
461464

462465
settings.setConvertSubtitles(convertSubsCheck.isSelected());
463466
settings.setSubtitleFormat(subtitleFormatCombo.getValue());

EncodeForge/src/main/java/com/encodeforge/model/ConversionSettings.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ public class ConversionSettings {
6666
private String audioTrackSelection = "all";
6767
private String audioLanguages = "eng";
6868
private String audioBitrate = "Auto";
69+
private boolean audioNormalize = false;
6970

7071
// Subtitle settings
7172
private boolean convertSubtitles = true;
@@ -178,6 +179,9 @@ public class ConversionSettings {
178179
public String getAudioBitrate() { return audioBitrate; }
179180
public void setAudioBitrate(String audioBitrate) { this.audioBitrate = audioBitrate; }
180181

182+
public boolean isAudioNormalize() { return audioNormalize; }
183+
public void setAudioNormalize(boolean audioNormalize) { this.audioNormalize = audioNormalize; }
184+
181185
public boolean isConvertSubtitles() { return convertSubtitles; }
182186
public void setConvertSubtitles(boolean convertSubtitles) { this.convertSubtitles = convertSubtitles; }
183187

@@ -439,6 +443,7 @@ public JsonObject toJson() {
439443
json.addProperty("audio_track_selection", audioTrackSelection);
440444
json.addProperty("audio_languages", audioLanguages);
441445
json.addProperty("audio_bitrate", audioBitrate);
446+
json.addProperty("audio_normalize", audioNormalize);
442447

443448
// Subtitles
444449
json.addProperty("convert_subtitles", convertSubtitles);

EncodeForge/src/main/resources/fxml/MainView.fxml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@
232232
<Label text="Processing Options" styleClass="group-label"/>
233233
<HBox spacing="15">
234234
<CheckBox fx:id="quickHwAccelCheck" text="Hardware Acceleration" styleClass="quick-check" selected="true"/>
235+
<CheckBox fx:id="quickAudioNormalizeCheck" text="Normalize Audio" styleClass="quick-check"/>
235236
<CheckBox fx:id="quickDownloadSubsCheck" text="Download Subtitles" styleClass="quick-check" disable="true"/>
236237
<CheckBox fx:id="quickRenameCheck" text="Rename Files" styleClass="quick-check" disable="true"/>
237238
</HBox>

EncodeForge/src/main/resources/fxml/SettingsDialog.fxml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,13 @@
175175
<Label text="Audio Bitrate:" styleClass="setting-label"/>
176176
<ComboBox fx:id="audioBitrateCombo" maxWidth="Infinity"/>
177177
</VBox>
178+
179+
<Separator/>
180+
181+
<VBox spacing="10">
182+
<CheckBox fx:id="audioNormalizeCheck" text="Normalize audio volume" selected="false"/>
183+
<Label text="Automatically adjust audio levels to a consistent volume across all files" styleClass="hint-label"/>
184+
</VBox>
178185
</VBox>
179186

180187
<!-- Subtitle Settings -->

EncodeForge/src/main/resources/python/encodeforge_api.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ def __init__(self):
163163
# Audio settings
164164
self.audio_codec = "copy"
165165
self.audio_bitrate = None
166+
self.normalize_audio = False
166167

167168
# General settings
168169
self.output_format = "mp4"
@@ -1491,6 +1492,8 @@ def update_settings(self, settings_dict: Dict):
14911492
self.settings.audio_bitrate = bitrate
14921493
else:
14931494
self.settings.audio_bitrate = None
1495+
if "audio_normalize" in settings_dict:
1496+
self.settings.normalize_audio = settings_dict["audio_normalize"]
14941497
if "audio_languages" in settings_dict:
14951498
# Store for potential future use
14961499
pass

EncodeForge/src/main/resources/python/encodeforge_modules/conversion_handler.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,11 @@ def _retry_with_software_encoder(self, input_file: Path, output_path_obj: Path,
510510
if self.settings.audio_bitrate:
511511
cmd.extend(["-b:a", self.settings.audio_bitrate])
512512

513+
# Add audio normalization filter
514+
if self.settings.normalize_audio:
515+
logger.info("Applying audio normalization (loudnorm)")
516+
cmd.extend(["-af", "loudnorm=I=-16:TP=-1.5:LRA=11"])
517+
513518
# Add faststart for MP4
514519
if self.settings.use_faststart and self.settings.output_format in ["mp4", "m4v"]:
515520
cmd.extend(["-movflags", "+faststart"])
@@ -722,6 +727,11 @@ def convert_file(
722727
if self.settings.audio_bitrate and self.settings.audio_bitrate != "Auto":
723728
cmd.extend(["-b:a", self.settings.audio_bitrate])
724729

730+
# Add audio normalization filter
731+
if self.settings.normalize_audio:
732+
logger.info("Applying audio normalization (loudnorm)")
733+
cmd.extend(["-af", "loudnorm=I=-16:TP=-1.5:LRA=11"])
734+
725735
# Subtitle handling with comprehensive format support
726736
temp_subtitle_files = []
727737
subtitle_tracks = [] # Initialize to avoid unbound variable

EncodeForge/src/main/resources/python/subtitle_providers/subdl_provider.py

Lines changed: 120 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"""
33
SubDL Provider
44
Good for movies and TV shows with free API
5-
Updated to use latest API structure
5+
Updated to use latest API structure with web scraping fallback
66
"""
77

88
import gzip
@@ -16,6 +16,13 @@
1616
from io import BytesIO
1717
from typing import Dict, List
1818

19+
try:
20+
from bs4 import BeautifulSoup
21+
BS4_AVAILABLE = True
22+
except ImportError:
23+
BS4_AVAILABLE = False
24+
logging.getLogger(__name__).warning("BeautifulSoup not available for SubDL web scraping fallback")
25+
1926
from .base_provider import BaseSubtitleProvider
2027

2128
logger = logging.getLogger(__name__)
@@ -27,6 +34,9 @@ class SubDLProvider(BaseSubtitleProvider):
2734
def __init__(self):
2835
super().__init__()
2936
self.provider_name = "SubDL"
37+
# SubDL provides free API keys without login (no rate limits on free keys)
38+
# This key was obtained from their website and works without authentication
39+
self.api_key = "0kzwwW12CndUyeuPru5DtdRwpIXfH9H9"
3040

3141
def search(self, video_path: str, languages: List[str]) -> List[Dict]:
3242
"""Search SubDL (subdl.com) - improved API integration"""
@@ -70,7 +80,7 @@ def search(self, video_path: str, languages: List[str]) -> List[Dict]:
7080
if season and episode:
7181
url = f"https://api.subdl.com/api/v1/subtitles"
7282
params = {
73-
'api_key': 'free',
83+
'api_key': self.api_key,
7484
'film_name': search_term,
7585
'type': 'tv',
7686
'season_number': str(season),
@@ -86,7 +96,7 @@ def search(self, video_path: str, languages: List[str]) -> List[Dict]:
8696
# Also try with episode in the name
8797
search_with_ep = f"{search_term} S{season:02d}E{episode:02d}"
8898
params2 = {
89-
'api_key': 'free',
99+
'api_key': self.api_key,
90100
'film_name': search_with_ep,
91101
'languages': ','.join(subdl_languages)
92102
}
@@ -99,7 +109,7 @@ def search(self, video_path: str, languages: List[str]) -> List[Dict]:
99109
# Movie search
100110
url = f"https://api.subdl.com/api/v1/subtitles"
101111
params = {
102-
'api_key': 'free',
112+
'api_key': self.api_key,
103113
'film_name': search_term,
104114
'type': 'movie',
105115
'languages': ','.join(subdl_languages)
@@ -114,7 +124,7 @@ def search(self, video_path: str, languages: List[str]) -> List[Dict]:
114124

115125
# Also try without specifying type
116126
params_general = {
117-
'api_key': 'free',
127+
'api_key': self.api_key,
118128
'film_name': search_term,
119129
'languages': ','.join(subdl_languages)
120130
}
@@ -167,11 +177,20 @@ def search(self, video_path: str, languages: List[str]) -> List[Dict]:
167177

168178
except urllib.error.HTTPError as e:
169179
logger.warning(f"SubDL HTTP error {e.code}: {e.reason}")
180+
if e.code == 403:
181+
logger.info("SubDL API returned 403, will try web scraping fallback")
170182
continue
171183
except Exception as e:
172184
logger.warning(f"SubDL search failed: {e}")
173185
continue
174186

187+
# If API failed (403 or no results), try web scraping fallback
188+
if (not data or not data.get('subtitles')) and BS4_AVAILABLE:
189+
logger.info("SubDL API search failed, trying web scraping fallback...")
190+
results = self._search_web_fallback(search_term, season, episode, subdl_languages)
191+
if results:
192+
return results
193+
175194
if not data or not data.get('subtitles'):
176195
logger.info("SubDL: No results found after all search strategies")
177196
return results
@@ -218,6 +237,102 @@ def search(self, video_path: str, languages: List[str]) -> List[Dict]:
218237

219238
return results
220239

240+
def _search_web_fallback(self, search_term: str, season, episode, languages: List[str]) -> List[Dict]:
241+
"""Web scraping fallback when API returns 403"""
242+
results = []
243+
244+
if not BS4_AVAILABLE:
245+
return results
246+
247+
try:
248+
# Build search URL
249+
query = search_term.replace(' ', '-').lower()
250+
if season and episode:
251+
search_url = f"https://subdl.com/subtitle/{query}/english"
252+
else:
253+
search_url = f"https://subdl.com/subtitle/{query}/english"
254+
255+
logger.info(f"SubDL web scraping: {search_url}")
256+
257+
headers = {
258+
'User-Agent': self.session_headers['User-Agent'],
259+
'Accept': 'text/html,application/xhtml+xml',
260+
'Accept-Language': 'en-US,en;q=0.9'
261+
}
262+
263+
req = urllib.request.Request(search_url, headers=headers)
264+
time.sleep(1.0) # Be respectful
265+
266+
with urllib.request.urlopen(req, timeout=15) as response:
267+
html = response.read().decode('utf-8', errors='ignore')
268+
269+
soup = BeautifulSoup(html, 'html.parser')
270+
271+
# Find subtitle entries
272+
subtitle_entries = soup.find_all('div', class_='item') or soup.find_all('a', class_='subtitle-entry')
273+
274+
for entry in subtitle_entries[:20]:
275+
try:
276+
# Extract subtitle info from web page
277+
name_elem = entry.find('h5') or entry.find('span', class_='title') or entry.find('a')
278+
if not name_elem:
279+
continue
280+
281+
file_name = name_elem.get_text(strip=True)
282+
283+
# Get download link
284+
link_elem = entry.find('a', href=True)
285+
if link_elem:
286+
download_url = link_elem['href']
287+
if not download_url.startswith('http'):
288+
download_url = 'https://subdl.com' + download_url
289+
else:
290+
download_url = ''
291+
292+
# Extract file ID from URL
293+
file_id = download_url.split('/')[-1] if download_url else ''
294+
295+
# Try to extract language
296+
lang_elem = entry.find('span', class_='language') or entry.find('span', class_='lang')
297+
lang_code = 'en' # default
298+
if lang_elem:
299+
lang_text = lang_elem.get_text(strip=True).lower()
300+
if 'spanish' in lang_text or 'español' in lang_text:
301+
lang_code = 'es'
302+
elif 'french' in lang_text or 'français' in lang_text:
303+
lang_code = 'fr'
304+
elif 'german' in lang_text or 'deutsch' in lang_text:
305+
lang_code = 'de'
306+
307+
# Filter by season/episode if TV show
308+
if season and episode:
309+
se_pattern = f"s{season:02d}e{episode:02d}"
310+
if se_pattern not in file_name.lower():
311+
continue
312+
313+
results.append({
314+
"provider": "SubDL",
315+
"file_name": file_name,
316+
"language": lang_code,
317+
"downloads": 0,
318+
"rating": 0,
319+
"file_id": file_id,
320+
"download_url": download_url,
321+
"movie_name": search_term,
322+
"format": "srt"
323+
})
324+
325+
except Exception as e:
326+
logger.debug(f"Error parsing SubDL web entry: {e}")
327+
continue
328+
329+
logger.info(f"SubDL web scraping found {len(results)} subtitle(s)")
330+
331+
except Exception as e:
332+
logger.warning(f"SubDL web scraping failed: {e}")
333+
334+
return results
335+
221336
def download(self, file_id: str, download_url: str, output_path: str) -> tuple:
222337
"""Download from SubDL - improved with ZIP handling"""
223338
try:

0 commit comments

Comments
 (0)