Skip to content

Commit 7bd6efd

Browse files
authored
feat(#394): add studio one project scanning support
1 parent 4c2a873 commit 7bd6efd

9 files changed

Lines changed: 696 additions & 5 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
# Owlplug specific
1212
build/input/
1313
build/output/
14+
temp/
1415
src/main/resources/credentials.properties
1516

1617
# OwlPlug Host binaries

owlplug-client/src/main/java/com/owlplug/core/components/ApplicationDefaults.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ public Image getDAWApplicationIcon(DawApplication application) {
169169
return switch (application) {
170170
case ABLETON -> abletonLogoImage;
171171
case REAPER -> reaperLogoImage;
172+
case STUDIO_ONE -> pluginComponentImage; // Using generic plugin icon as placeholder
172173
};
173174
}
174175

owlplug-client/src/main/java/com/owlplug/core/utils/ArchiveUtils.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.io.InputStream;
2626
import java.io.OutputStream;
2727
import java.nio.file.Files;
28+
import java.util.Collection;
2829
import org.apache.commons.compress.archivers.ArchiveEntry;
2930
import org.apache.commons.compress.archivers.ArchiveException;
3031
import org.apache.commons.compress.archivers.ArchiveInputStream;
@@ -126,4 +127,39 @@ private static void uncompress(ArchiveInputStream o, File destinationDirectory)
126127
}
127128
}
128129

130+
/**
131+
* Extract only specific files from an archive.
132+
* This is useful when you only need a subset of files and want to avoid
133+
* creating directories with reserved names (e.g., Windows "Strings" directory).
134+
*
135+
* @param archive the archive file to extract from
136+
* @param dest the destination directory
137+
* @param targetPaths collection of file paths within the archive to extract (e.g., "metainfo.xml", "Devices/audiomixer.xml")
138+
* @throws IOException if extraction fails
139+
*/
140+
public static void extractFiles(File archive, File dest, Collection<String> targetPaths) throws IOException {
141+
try (InputStream fi = new BufferedInputStream(new FileInputStream(archive));
142+
ArchiveInputStream ais = new ArchiveStreamFactory().createArchiveInputStream(fi)) {
143+
144+
ArchiveEntry entry;
145+
while ((entry = ais.getNextEntry()) != null) {
146+
String entryName = entry.getName().replace('\\', '/');
147+
if (!targetPaths.contains(entryName) || entry.isDirectory()) {
148+
continue;
149+
}
150+
151+
File outFile = new File(dest, entryName);
152+
if (!outFile.getParentFile().isDirectory() && !outFile.getParentFile().mkdirs()) {
153+
throw new IOException("Failed to create directories for file " + outFile.getAbsolutePath());
154+
}
155+
156+
try (OutputStream out = Files.newOutputStream(outFile.toPath())) {
157+
IOUtils.copy(ais, out);
158+
}
159+
}
160+
} catch (ArchiveException e) {
161+
throw new IOException("Failed to read archive: " + archive, e);
162+
}
163+
}
164+
129165
}

owlplug-client/src/main/java/com/owlplug/project/model/DawApplication.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020

2121
public enum DawApplication {
2222
ABLETON("Ableton"),
23-
REAPER("Reaper");
23+
REAPER("Reaper"),
24+
STUDIO_ONE("Studio One");
2425
private final String name;
2526

2627
DawApplication(String name) {

owlplug-client/src/main/java/com/owlplug/project/tasks/ProjectSyncTask.java

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import com.owlplug.project.repositories.DawProjectRepository;
2626
import com.owlplug.project.tasks.discovery.ableton.AbletonProjectExplorer;
2727
import com.owlplug.project.tasks.discovery.reaper.ReaperProjectExplorer;
28+
import com.owlplug.project.tasks.discovery.studioone.StudioOneProjectExplorer;
2829
import java.io.File;
2930
import java.util.ArrayList;
3031
import java.util.List;
@@ -72,19 +73,32 @@ protected TaskResult start() throws Exception {
7273

7374
this.setMaxProgress(filteredFiles.size());
7475

76+
// Create explorer instances once, outside the loop (they are stateless)
77+
AbletonProjectExplorer abletonExplorer = new AbletonProjectExplorer();
78+
ReaperProjectExplorer reaperExplorer = new ReaperProjectExplorer();
79+
StudioOneProjectExplorer studioOneExplorer = new StudioOneProjectExplorer();
80+
7581
for (File file : filteredFiles) {
7682
this.commitProgress(1);
77-
AbletonProjectExplorer abletonExplorer = new AbletonProjectExplorer();
78-
ReaperProjectExplorer reaperExplorer = new ReaperProjectExplorer();
7983

8084
if (abletonExplorer.canExploreFile(file)) {
8185
this.updateMessage("Analyzing Ableton file: " + file.getAbsolutePath());
8286
DawProject project = abletonExplorer.explore(file);
83-
projectRepository.save(project);
87+
if (project != null) {
88+
projectRepository.save(project);
89+
}
8490
} else if (reaperExplorer.canExploreFile(file)) {
8591
this.updateMessage("Analyzing Reaper file: " + file.getAbsolutePath());
8692
DawProject project = reaperExplorer.explore(file);
87-
projectRepository.save(project);
93+
if (project != null) {
94+
projectRepository.save(project);
95+
}
96+
} else if (studioOneExplorer.canExploreFile(file)) {
97+
this.updateMessage("Analyzing Studio One file: " + file.getAbsolutePath());
98+
DawProject project = studioOneExplorer.explore(file);
99+
if (project != null) {
100+
projectRepository.save(project);
101+
}
88102
}
89103
}
90104

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/* OwlPlug
2+
* Copyright (C) 2021 Arthur <dropsnorz@gmail.com>
3+
*
4+
* This file is part of OwlPlug.
5+
*
6+
* OwlPlug is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License version 3
8+
* as published by the Free Software Foundation.
9+
*
10+
* OwlPlug is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with OwlPlug. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
19+
package com.owlplug.project.tasks.discovery.studioone;
20+
21+
import com.owlplug.core.utils.PluginUtils;
22+
import com.owlplug.plugin.model.PluginFormat;
23+
import com.owlplug.project.model.DawPlugin;
24+
import java.util.ArrayList;
25+
import java.util.List;
26+
import javax.xml.xpath.XPath;
27+
import javax.xml.xpath.XPathConstants;
28+
import javax.xml.xpath.XPathExpressionException;
29+
import javax.xml.xpath.XPathFactory;
30+
import org.slf4j.Logger;
31+
import org.slf4j.LoggerFactory;
32+
import org.w3c.dom.Document;
33+
import org.w3c.dom.Element;
34+
import org.w3c.dom.Node;
35+
import org.w3c.dom.NodeList;
36+
37+
public class StudioOneAudioMixerPluginCollector {
38+
39+
private final Logger log = LoggerFactory.getLogger(this.getClass());
40+
41+
private Document document;
42+
43+
public StudioOneAudioMixerPluginCollector(Document document) {
44+
this.document = document;
45+
}
46+
47+
public List<DawPlugin> collectPlugins() {
48+
49+
ArrayList<DawPlugin> plugins = new ArrayList<>();
50+
51+
XPath xpath = XPathFactory.newInstance().newXPath();
52+
try {
53+
// Find all FX nodes in the document (all Inserts / channel groups)
54+
NodeList fxNodes = (NodeList) xpath
55+
.compile("//Attributes[starts-with(@name, 'FX')]")
56+
.evaluate(document, XPathConstants.NODESET);
57+
58+
for (int i = 0; i < fxNodes.getLength(); i++) {
59+
Node node = fxNodes.item(i);
60+
if (node instanceof Element element) {
61+
// Filter out empty FX slots by checking for deviceData or ghostData children
62+
StudioOneDomUtils.DeviceDataAndGhostData data = StudioOneDomUtils.findDeviceDataAndGhostData(element);
63+
if (data.getDeviceData() == null && data.getGhostData() == null) {
64+
continue; // Skip empty FX slots
65+
}
66+
67+
DawPlugin plugin = readPluginElement(element);
68+
if (plugin != null) {
69+
plugins.add(plugin);
70+
}
71+
}
72+
}
73+
74+
} catch (XPathExpressionException e) {
75+
log.error("Error extracting plugins from audio mixer", e);
76+
}
77+
78+
return plugins;
79+
}
80+
81+
private DawPlugin readPluginElement(Element pluginElement) {
82+
String pluginName = StudioOneDomUtils.extractPluginName(pluginElement);
83+
if (pluginName == null || pluginName.isEmpty()) {
84+
return null;
85+
}
86+
87+
PluginFormat format = StudioOneDomUtils.extractPluginFormat(pluginElement);
88+
if (format == null) {
89+
return null;
90+
}
91+
92+
// Normalize the plugin name (remove platform suffixes like x64, x32, etc.)
93+
// This ensures cleaner data in the database and simplifies later queries
94+
String normalizedName = PluginUtils.absoluteName(pluginName);
95+
96+
DawPlugin plugin = new DawPlugin();
97+
plugin.setName(normalizedName);
98+
plugin.setFormat(format);
99+
return plugin;
100+
}
101+
102+
}
103+

0 commit comments

Comments
 (0)