peertube-plugin-transposer-connector
Version:
Transposer connector is a PeerTube language tool plugin to transcribe and translate with Whisper
656 lines (591 loc) • 26.3 kB
JavaScript
const initLanguageMetadata = (router, languageMetadataDAO, peertubeHelpers) => {
// Helper function to get video details and extract numeric ID
const getVideoNumericId = async (idParam) => {
// If the ID is already numeric, return it directly
if (!isNaN(idParam) && Number.isInteger(Number(idParam))) {
peertubeHelpers.logger.info(
`[transposer-connector] LanguageMetadataController: ID ${idParam} is already numeric, using directly`
);
return Number(idParam);
}
try {
peertubeHelpers.logger.info(
`[transposer-connector] LanguageMetadataController: Converting UUID/slug ${idParam} to numeric ID via PeerTube API`
);
// Make a request to the videos API to get the full video object
const webserverUrl = peertubeHelpers.config.getWebserverUrl();
const response = await fetch(`${webserverUrl}/api/v1/videos/${idParam}`);
if (!response.ok) {
peertubeHelpers.logger.error(
`[transposer-connector] LanguageMetadataController: PeerTube API returned status ${response.status} for video ${idParam}`
);
throw new Error(`Video API returned status: ${response.status}`);
}
const data = await response.json();
// Extract the numeric ID from the response
if (data && data.id) {
peertubeHelpers.logger.info(
`[transposer-connector] LanguageMetadataController: Successfully converted ${idParam} to numeric ID ${data.id}`
);
return data.id;
}
throw new Error("Video ID not found in response");
} catch (error) {
peertubeHelpers.logger.error(
`[transposer-connector] LanguageMetadataController: Failed to resolve video ID for ${idParam}: ${error.message}`
);
throw new Error("Failed to resolve video ID");
}
};
// Retrieve Specific Languages / All Metadata:
router.get("/language/metadata/video/:id", async (req, res) => {
try {
const idParam = req.params.id;
peertubeHelpers.logger.info(
`[transposer-connector] LanguageMetadataController: Starting metadata retrieval request for video ${idParam}`
);
// Get the numeric ID
let numericVideoId;
try {
numericVideoId = await getVideoNumericId(idParam);
} catch (error) {
peertubeHelpers.logger.error(
`[transposer-connector] LanguageMetadataController: Video not found for ID ${idParam}`
);
return res.status(404).json({ error: "Video not found" });
}
const languages = req.query.languages
? req.query.languages.split(",")
: [];
const attributes = req.query.attribute
? req.query.attribute.split(",")
: [];
peertubeHelpers.logger.info(
`[transposer-connector] LanguageMetadataController: Fetching metadata for video ${numericVideoId}, languages: [${
languages.join(", ") || "all"
}], attributes: [${attributes.join(", ") || "all"}]`
);
const completeMetadata =
await languageMetadataDAO.findLanguageMetadataByIdAndLanguages(
numericVideoId,
languages
);
if (!completeMetadata) {
peertubeHelpers.logger.warn(
`[transposer-connector] LanguageMetadataController: No metadata found for video ${numericVideoId}`
);
return res
.status(404)
.json({ error: "Not Found: Resource not available" });
}
const transformedObject = {};
completeMetadata.forEach((item) => {
if (languages.length === 0 || languages.includes(item.language)) {
if (attributes.length === 0 || attributes.includes(item.attribute)) {
if (!transformedObject[item.attribute]) {
transformedObject[item.attribute] = {};
}
transformedObject[item.attribute][item.language] = item.value;
}
}
});
peertubeHelpers.logger.info(
`[transposer-connector] LanguageMetadataController: Successfully retrieved metadata for video ${numericVideoId}, returned ${
Object.keys(transformedObject).length
} attributes`
);
return res.json(transformedObject);
} catch (error) {
peertubeHelpers.logger.error(
`[transposer-connector] LanguageMetadataController: Exception during metadata retrieval for video ${req.params.id}: ${error.message}`
);
return res.status(500).json({ error: "Internal server error" });
}
});
// Retrieve All Languages / Specific Metadata:
router.get("/language/metadata/video/:id/:attribute", async (req, res) => {
try {
const idParam = req.params.id;
const attribute = req.params.attribute;
peertubeHelpers.logger.info(
`[transposer-connector] LanguageMetadataController: Starting specific attribute retrieval for video ${idParam}, attribute: ${attribute}`
);
// Get the numeric ID
let numericVideoId;
try {
numericVideoId = await getVideoNumericId(idParam);
} catch (error) {
peertubeHelpers.logger.error(
`[transposer-connector] LanguageMetadataController: Video not found for ID ${idParam}`
);
return res.status(404).json({ error: "Video not found" });
}
const specificMetadata =
await languageMetadataDAO.findSpecificLanguageMetadata(
numericVideoId,
attribute
);
if (!specificMetadata) {
peertubeHelpers.logger.warn(
`[transposer-connector] LanguageMetadataController: No metadata found for video ${numericVideoId}, attribute ${attribute}`
);
return res
.status(404)
.json({ error: "Not Found: Resource not available" });
}
const transformedObject = {};
specificMetadata.forEach((item) => {
transformedObject[item.language] = item.value;
});
peertubeHelpers.logger.info(
`[transposer-connector] LanguageMetadataController: Successfully retrieved specific metadata for video ${numericVideoId}, attribute ${attribute}, returned ${
Object.keys(transformedObject).length
} languages`
);
return res.json(transformedObject);
} catch (error) {
peertubeHelpers.logger.error(
`[transposer-connector] LanguageMetadataController: Exception during specific metadata retrieval for video ${req.params.id}, attribute ${req.params.attribute}: ${error.message}`
);
return res.status(500).json({ error: "Internal server error" });
}
});
router.get("/video/:id/complete", async (req, res) => {
try {
const idParam = req.params.id;
peertubeHelpers.logger.info(
`[transposer-connector] LanguageMetadataController: Starting complete video data retrieval for video ${idParam}`
);
// Get the numeric ID
let numericVideoId;
try {
numericVideoId = await getVideoNumericId(idParam);
} catch (error) {
peertubeHelpers.logger.error(
`[transposer-connector] LanguageMetadataController: Video not found for ID ${idParam}`
);
return res.status(404).json({ error: "Video not found" });
}
// Get query parameters for filtering
const languages = req.query.languages
? req.query.languages.split(",")
: [];
const attributes = req.query.attribute
? req.query.attribute.split(",")
: [];
peertubeHelpers.logger.info(
`[transposer-connector] LanguageMetadataController: Fetching complete data for video ${numericVideoId}, languages filter: [${
languages.join(", ") || "all"
}], attributes filter: [${attributes.join(", ") || "all"}]`
);
// Fetch metadata
let metadata = {};
try {
const completeMetadata =
await languageMetadataDAO.findLanguageMetadataByIdAndLanguages(
numericVideoId,
languages
);
if (completeMetadata && completeMetadata.length > 0) {
const transformedMetadata = {};
completeMetadata.forEach((item) => {
if (languages.length === 0 || languages.includes(item.language)) {
if (
attributes.length === 0 ||
attributes.includes(item.attribute)
) {
if (!transformedMetadata[item.attribute]) {
transformedMetadata[item.attribute] = {};
}
transformedMetadata[item.attribute][item.language] = item.value;
}
}
});
metadata = transformedMetadata;
peertubeHelpers.logger.info(
`[transposer-connector] LanguageMetadataController: Retrieved ${
Object.keys(metadata).length
} metadata attributes for video ${numericVideoId}`
);
} else {
peertubeHelpers.logger.info(
`[transposer-connector] LanguageMetadataController: No metadata found for video ${numericVideoId}`
);
}
} catch (error) {
peertubeHelpers.logger.error(
`[transposer-connector] LanguageMetadataController: Error fetching metadata for video ${numericVideoId}: ${error.message}`
);
metadata = {};
}
// Fetch captions
let captions = [];
try {
peertubeHelpers.logger.info(
`[transposer-connector] LanguageMetadataController: Fetching captions for video ${numericVideoId}`
);
const webserverUrl = peertubeHelpers.config.getWebserverUrl();
const captionsResponse = await fetch(
`${webserverUrl}/api/v1/videos/${numericVideoId}/captions`
);
if (captionsResponse.ok) {
const captionsData = await captionsResponse.json();
captions = captionsData.data || [];
// Filter captions by language if specified
if (languages.length > 0) {
const originalCount = captions.length;
captions = captions.filter((caption) =>
languages.includes(caption.language.id)
);
peertubeHelpers.logger.info(
`[transposer-connector] LanguageMetadataController: Filtered captions for video ${numericVideoId}, ${originalCount} -> ${captions.length} captions after language filter`
);
} else {
peertubeHelpers.logger.info(
`[transposer-connector] LanguageMetadataController: Retrieved ${captions.length} captions for video ${numericVideoId}`
);
}
} else {
peertubeHelpers.logger.warn(
`[transposer-connector] LanguageMetadataController: Failed to fetch captions for video ${numericVideoId}, status: ${captionsResponse.status}`
);
}
} catch (error) {
peertubeHelpers.logger.error(
`[transposer-connector] LanguageMetadataController: Exception while fetching captions for video ${numericVideoId}: ${error.message}`
);
captions = [];
}
// Combine the data
const combinedData = {
videoId: numericVideoId,
metadata: metadata,
captions: {
total: captions.length,
data: captions,
},
availableLanguages: {
metadata: metadata
? Object.keys(metadata).reduce((acc, attr) => {
Object.keys(metadata[attr]).forEach((lang) => {
if (!acc.includes(lang)) acc.push(lang);
});
return acc;
}, [])
: [],
captions: captions.map((caption) => caption.language.id),
},
};
peertubeHelpers.logger.info(
`[transposer-connector] LanguageMetadataController: Successfully compiled complete data for video ${numericVideoId}, metadata attributes: ${
Object.keys(metadata).length
}, captions: ${captions.length}`
);
return res.json(combinedData);
} catch (error) {
peertubeHelpers.logger.error(
`[transposer-connector] LanguageMetadataController: Exception during complete video data retrieval for video ${req.params.id}: ${error.message}`
);
return res.status(500).json({ error: "Internal server error" });
}
});
// NEW: Get combined video data with caption content
router.get("/video/:id/complete-with-content", async (req, res) => {
try {
const idParam = req.params.id;
peertubeHelpers.logger.info(
`[transposer-connector] LanguageMetadataController: Starting complete video data with content retrieval for video ${idParam}`
);
// Get the numeric ID
let numericVideoId;
try {
numericVideoId = await getVideoNumericId(idParam);
} catch (error) {
peertubeHelpers.logger.error(
`[transposer-connector] LanguageMetadataController: Video not found for ID ${idParam}`
);
return res.status(404).json({ error: "Video not found" });
}
// Get query parameters for filtering
const languages = req.query.languages
? req.query.languages.split(",")
: [];
const attributes = req.query.attribute
? req.query.attribute.split(",")
: [];
peertubeHelpers.logger.info(
`[transposer-connector] LanguageMetadataController: Fetching complete data with content for video ${numericVideoId}, languages filter: [${
languages.join(", ") || "all"
}], attributes filter: [${attributes.join(", ") || "all"}]`
);
// Fetch metadata (same as above)
let metadata = {};
try {
const completeMetadata =
await languageMetadataDAO.findLanguageMetadataByIdAndLanguages(
numericVideoId,
languages
);
if (completeMetadata && completeMetadata.length > 0) {
const transformedMetadata = {};
completeMetadata.forEach((item) => {
if (languages.length === 0 || languages.includes(item.language)) {
if (
attributes.length === 0 ||
attributes.includes(item.attribute)
) {
if (!transformedMetadata[item.attribute]) {
transformedMetadata[item.attribute] = {};
}
transformedMetadata[item.attribute][item.language] = item.value;
}
}
});
metadata = transformedMetadata;
peertubeHelpers.logger.info(
`[transposer-connector] LanguageMetadataController: Retrieved ${
Object.keys(metadata).length
} metadata attributes for video ${numericVideoId}`
);
}
} catch (error) {
peertubeHelpers.logger.error(
`[transposer-connector] LanguageMetadataController: Error fetching metadata for video ${numericVideoId}: ${error.message}`
);
metadata = {};
}
// Fetch captions with content
let captions = [];
try {
peertubeHelpers.logger.info(
`[transposer-connector] LanguageMetadataController: Fetching captions with content for video ${numericVideoId}`
);
const webserverUrl = peertubeHelpers.config.getWebserverUrl();
const captionsResponse = await fetch(
`${webserverUrl}/api/v1/videos/${numericVideoId}/captions`
);
if (captionsResponse.ok) {
const captionsData = await captionsResponse.json();
let captionsList = captionsData.data || [];
// Filter captions by language if specified
if (languages.length > 0) {
const originalCount = captionsList.length;
captionsList = captionsList.filter((caption) =>
languages.includes(caption.language.id)
);
peertubeHelpers.logger.info(
`[transposer-connector] LanguageMetadataController: Filtered captions for video ${numericVideoId}, ${originalCount} -> ${captionsList.length} captions after language filter`
);
}
// Fetch content for each caption
peertubeHelpers.logger.info(
`[transposer-connector] LanguageMetadataController: Fetching content for ${captionsList.length} caption files for video ${numericVideoId}`
);
for (const caption of captionsList) {
try {
const captionContentResponse = await fetch(
`${webserverUrl}${caption.captionPath}`
);
if (captionContentResponse.ok) {
caption.content = await captionContentResponse.text();
peertubeHelpers.logger.info(
`[transposer-connector] LanguageMetadataController: Successfully fetched content for caption ${caption.language.id} for video ${numericVideoId}`
);
} else {
caption.content = null;
caption.contentError = `Failed to fetch content: ${captionContentResponse.status}`;
peertubeHelpers.logger.warn(
`[transposer-connector] LanguageMetadataController: Failed to fetch caption content for ${caption.language.id} for video ${numericVideoId}, status: ${captionContentResponse.status}`
);
}
} catch (error) {
caption.content = null;
caption.contentError = `Error fetching content: ${error.message}`;
peertubeHelpers.logger.error(
`[transposer-connector] LanguageMetadataController: Exception fetching caption content for ${caption.language.id} for video ${numericVideoId}: ${error.message}`
);
}
}
captions = captionsList;
} else {
peertubeHelpers.logger.warn(
`[transposer-connector] LanguageMetadataController: Failed to fetch captions for video ${numericVideoId}, status: ${captionsResponse.status}`
);
}
} catch (error) {
peertubeHelpers.logger.error(
`[transposer-connector] LanguageMetadataController: Exception while fetching captions with content for video ${numericVideoId}: ${error.message}`
);
captions = [];
}
// Combine the data
const combinedData = {
videoId: numericVideoId,
metadata: metadata,
captions: {
total: captions.length,
data: captions,
},
availableLanguages: {
metadata: metadata
? Object.keys(metadata).reduce((acc, attr) => {
Object.keys(metadata[attr]).forEach((lang) => {
if (!acc.includes(lang)) acc.push(lang);
});
return acc;
}, [])
: [],
captions: captions.map((caption) => caption.language.id),
},
};
peertubeHelpers.logger.info(
`[transposer-connector] LanguageMetadataController: Successfully compiled complete data with content for video ${numericVideoId}, metadata attributes: ${
Object.keys(metadata).length
}, captions with content: ${captions.length}`
);
return res.json(combinedData);
} catch (error) {
peertubeHelpers.logger.error(
`[transposer-connector] LanguageMetadataController: Exception during complete video data with content retrieval for video ${req.params.id}: ${error.message}`
);
return res.status(500).json({ error: "Internal server error" });
}
});
// Send all language metadata at once to the db
router.post("/language/metadata/video/:id/metadata", async (req, res) => {
try {
const idParam = req.params.id;
peertubeHelpers.logger.info(
`[transposer-connector] LanguageMetadataController: Starting metadata POST request for video ${idParam}`
);
let authUser;
try {
peertubeHelpers.logger.info(
`[transposer-connector] LanguageMetadataController: Attempting to authenticate user for video ${idParam} metadata update`
);
authUser = await peertubeHelpers.user.getAuthUser(res);
if (!authUser) {
peertubeHelpers.logger.warn(
`[transposer-connector] LanguageMetadataController: Authentication failed for video ${idParam} metadata update - no user found`
);
return res.status(401).json({ error: "Authentication required" });
}
peertubeHelpers.logger.info(
`[transposer-connector] LanguageMetadataController: User authenticated for video ${idParam}: ${authUser.username} (ID: ${authUser.id}, role: ${authUser.role})`
);
} catch (error) {
peertubeHelpers.logger.error(
`[transposer-connector] LanguageMetadataController: Authentication error for video ${idParam}: ${error.message}`
);
return res.status(401).json({ error: "Authentication failed" });
}
const metadata = req.body;
peertubeHelpers.logger.info(
`[transposer-connector] LanguageMetadataController: Validating metadata payload for video ${idParam}`
);
if (!metadata || typeof metadata !== "object") {
peertubeHelpers.logger.warn(
`[transposer-connector] LanguageMetadataController: Invalid metadata format provided for video ${idParam}`
);
return res.status(400).json({ error: "Invalid metadata format" });
}
const languageCount = Object.keys(metadata).length;
peertubeHelpers.logger.info(
`[transposer-connector] LanguageMetadataController: Metadata validation passed for video ${idParam}, processing ${languageCount} languages`
);
let numericVideoId;
try {
numericVideoId = await getVideoNumericId(idParam);
} catch (error) {
peertubeHelpers.logger.error(
`[transposer-connector] LanguageMetadataController: Video not found for ID ${idParam} during metadata update`
);
return res.status(404).json({ error: "Video not found" });
}
let video;
try {
peertubeHelpers.logger.info(
`[transposer-connector] LanguageMetadataController: Loading video details for ownership check, video ${numericVideoId}`
);
video = await peertubeHelpers.videos.loadByIdOrUUID(numericVideoId);
if (!video) {
peertubeHelpers.logger.warn(
`[transposer-connector] LanguageMetadataController: Video ${numericVideoId} not found during ownership verification`
);
return res.status(404).json({ error: "Video not found" });
}
peertubeHelpers.logger.info(
`[transposer-connector] LanguageMetadataController: Video loaded for ownership check: "${video.name}" (channel: ${video.VideoChannel?.name})`
);
} catch (error) {
peertubeHelpers.logger.error(
`[transposer-connector] LanguageMetadataController: Failed to load video ${numericVideoId} for ownership check: ${error.message}`
);
return res.status(500).json({ error: "Failed to load video details" });
}
peertubeHelpers.logger.info(
`[transposer-connector] LanguageMetadataController: Checking permissions for video ${numericVideoId} - user account: ${authUser.Account?.id}, video channel account: ${video.VideoChannel?.Account?.id}`
);
const isVideoOwner =
video.VideoChannel?.Account?.id === authUser.Account?.id;
const isAdmin = authUser.role === 0;
if (!isVideoOwner && !isAdmin) {
peertubeHelpers.logger.warn(
`[transposer-connector] LanguageMetadataController: Permission denied for user ${authUser.username} to modify video ${numericVideoId} metadata (not owner or admin)`
);
return res.status(403).json({
error: "You don't have permission to modify this video's metadata",
});
}
peertubeHelpers.logger.info(
`[transposer-connector] LanguageMetadataController: Permission granted for video ${numericVideoId} metadata update (user: ${authUser.username}, isOwner: ${isVideoOwner}, isAdmin: ${isAdmin})`
);
// Process and validate metadata
let totalAttributes = 0;
Object.keys(metadata).forEach((langCode) => {
if (typeof metadata[langCode] !== "object") {
peertubeHelpers.logger.error(
`[transposer-connector] LanguageMetadataController: Invalid format for language '${langCode}' in video ${numericVideoId} metadata`
);
return res.status(400).json({
error: `Invalid format for language '${langCode}'`,
});
}
const attributeCount = Object.keys(metadata[langCode]).length;
totalAttributes += attributeCount;
peertubeHelpers.logger.info(
`[transposer-connector] LanguageMetadataController: Processing ${attributeCount} attributes for language '${langCode}' in video ${numericVideoId}`
);
Object.keys(metadata[langCode]).forEach((field) => {
if (
metadata[langCode][field] === null ||
metadata[langCode][field] === undefined
) {
metadata[langCode][field] = "";
}
metadata[langCode][field] = String(metadata[langCode][field]).trim();
});
});
peertubeHelpers.logger.info(
`[transposer-connector] LanguageMetadataController: Saving ${totalAttributes} total attributes across ${languageCount} languages to database for video ${numericVideoId}`
);
const metadataId = await languageMetadataDAO.addLanguagedMetadata(
numericVideoId,
metadata
);
peertubeHelpers.logger.info(
`[transposer-connector] LanguageMetadataController: Successfully saved metadata for video ${numericVideoId} by user ${authUser.username}`
);
return res.json({ metadataId });
} catch (error) {
const idParam = req.params.id;
peertubeHelpers.logger.error(
`[transposer-connector] LanguageMetadataController: Exception in metadata POST endpoint for video ${idParam}: ${error.message}`
);
return res.status(500).json({ error: "Internal server error" });
}
});
};
module.exports = {
initLanguageMetadata,
};