Java tutorial
/** * Copyright 2009, 2010 The Regents of the University of California * Licensed under the Educational Community License, Version 2.0 * (the "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.osedu.org/licenses/ECL-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an "AS IS" * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing * permissions and limitations under the License. * */ package org.opencastproject.distribution.youtube; import static org.opencastproject.mediapackage.MediaPackageElements.YOUTUBE; import static org.opencastproject.mediapackage.Track.TYPE; import org.opencastproject.deliver.youtube.YoutubeConfiguration; import org.opencastproject.distribution.api.DistributionException; import org.opencastproject.distribution.api.DistributionService; import org.opencastproject.job.api.AbstractJobProducer; import org.opencastproject.job.api.Job; import org.opencastproject.mediapackage.MediaPackage; import org.opencastproject.mediapackage.MediaPackageElement; import org.opencastproject.mediapackage.MediaPackageElementBuilderFactory; import org.opencastproject.mediapackage.MediaPackageElementParser; import org.opencastproject.mediapackage.MediaPackageException; import org.opencastproject.mediapackage.MediaPackageParser; import org.opencastproject.mediapackage.Track; import org.opencastproject.security.api.OrganizationDirectoryService; import org.opencastproject.security.api.SecurityService; import org.opencastproject.security.api.UserDirectoryService; import org.opencastproject.serviceregistry.api.ServiceRegistry; import org.opencastproject.serviceregistry.api.ServiceRegistryException; import org.opencastproject.workflow.api.WorkflowService; import org.opencastproject.workspace.api.Workspace; import com.google.gdata.client.media.MediaService; import com.google.gdata.client.youtube.YouTubeService; import com.google.gdata.data.PlainTextConstruct; import com.google.gdata.data.media.MediaFileSource; import com.google.gdata.data.media.mediarss.MediaCategory; import com.google.gdata.data.media.mediarss.MediaDescription; import com.google.gdata.data.media.mediarss.MediaKeywords; import com.google.gdata.data.media.mediarss.MediaTitle; import com.google.gdata.data.youtube.PlaylistEntry; import com.google.gdata.data.youtube.PlaylistLinkEntry; import com.google.gdata.data.youtube.PlaylistLinkFeed; import com.google.gdata.data.youtube.VideoEntry; import com.google.gdata.data.youtube.VideoFeed; import com.google.gdata.data.youtube.YouTubeMediaGroup; import com.google.gdata.data.youtube.YouTubeNamespace; import com.google.gdata.data.youtube.YtPublicationState; import com.google.gdata.util.ServiceException; import org.apache.commons.lang.StringUtils; import org.osgi.service.cm.ConfigurationException; import org.osgi.service.cm.ManagedService; import org.osgi.service.component.ComponentContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.URI; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Dictionary; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Distributes media to a Youtube play list. */ public class YoutubeDistributionService extends AbstractJobProducer implements DistributionService, ManagedService { /** Receipt type */ public static final String JOB_TYPE = "org.opencastproject.distribution.youtube"; /** Time to wait between polling for status (milliseconds.) */ private static final long POLL_MILLISECONDS = 30L * 1000L; /** Base URL of playlists FIXME: Should be read in from properties file */ private static final String PLAYLIST_URL = "http://gdata.youtube.com/feeds/api/users/{username}/playlists"; /** Base URL of video entries FIXME: Should be read in from properties file */ private static final String VIDEO_ENTRY_URL = "http://uploads.gdata.youtube.com/feeds/api/users/default/uploads"; /** Base URL of feed entries */ private static final String VIDEO_FEED_URL = "http://gdata.youtube.com/feeds/api/users/{username}/uploads"; /** logger instance */ private static final Logger logger = LoggerFactory.getLogger(YoutubeDistributionService.class); /** List of available operations on jobs */ private enum Operation { Distribute, Retract }; /** workspace instance */ protected Workspace workspace = null; /** workflow service instance */ protected WorkflowService workflowService = null; /** The remote service registry */ protected ServiceRegistry serviceRegistry = null; /** Youtube configuration instance */ protected static YoutubeConfiguration config = null; /** The organization directory service */ private OrganizationDirectoryService organizationDirectoryService; /** The user directory service */ private UserDirectoryService userDirectoryService; /** The security service */ private SecurityService securityService; /** * The default playlist to distribute to, in case there is not enough information in the mediapackage to find a * playlist */ private String defaultPlaylist; /** chunk youtube upload */ private boolean isDefaultChunked = true; /** * Creates a new instance of the youtube distribution service. */ public YoutubeDistributionService() { super(JOB_TYPE); } /** * Called when service activates. Defined in OSGi resource file. */ public synchronized void activate(ComponentContext cc) { config = YoutubeConfiguration.getInstance(); } /** * {@inheritDoc} * * @see org.osgi.service.cm.ManagedService#updated(java.util.Dictionary) */ @Override public synchronized void updated(Dictionary properties) throws ConfigurationException { logger.info("Update method from managed service"); String username = StringUtils .trimToNull((String) properties.get("org.opencastproject.distribution.youtube.username")); String password = StringUtils .trimToNull((String) properties.get("org.opencastproject.distribution.youtube.password")); String clientid = StringUtils .trimToNull((String) properties.get("org.opencastproject.distribution.youtube.clientid")); String developerkey = StringUtils .trimToNull((String) properties.get("org.opencastproject.distribution.youtube.developerkey")); String category = StringUtils .trimToNull((String) properties.get("org.opencastproject.distribution.youtube.category")); String keywords = StringUtils .trimToNull((String) properties.get("org.opencastproject.distribution.youtube.keywords")); String privacy = StringUtils .trimToNull((String) properties.get("org.opencastproject.distribution.youtube.private")); String isChunked = StringUtils .trimToNull((String) properties.get("org.opencastproject.distribution.youtube.chunked")); defaultPlaylist = StringUtils .trimToNull((String) properties.get("org.opencastproject.distribution.youtube.default.playlist")); // Setup configuration from properties if (StringUtils.isBlank(clientid)) throw new IllegalArgumentException("Youtube clientid must be specified"); if (StringUtils.isBlank(developerkey)) throw new IllegalArgumentException("Youtube developerkey must be specified"); if (StringUtils.isBlank(username)) throw new IllegalArgumentException("Youtube username must be specified"); if (StringUtils.isBlank(password)) throw new IllegalArgumentException("Youtube password must be specified"); if (StringUtils.isBlank(category)) throw new IllegalArgumentException("Youtube category must be specified"); if (StringUtils.isBlank(keywords)) throw new IllegalArgumentException("Youtube keywords must be specified"); if (StringUtils.isBlank(privacy)) throw new IllegalArgumentException("Youtube privacy must be specified"); String uploadUrl = VIDEO_ENTRY_URL.replaceAll("\\{username\\}", username); config.setClientId(clientid); config.setDeveloperKey(developerkey); config.setUserId(username); config.setUploadUrl(uploadUrl); config.setPassword(password); config.setKeywords(keywords); config.setCategory(category); config.setVideoPrivate(Boolean.getBoolean(privacy)); if (StringUtils.isNotBlank(isChunked)) isDefaultChunked = Boolean.getBoolean(isChunked); } /** * {@inheritDoc} * * @see org.opencastproject.distribution.api.DistributionService#distribute(MediaPackage, String) */ @Override public Job distribute(MediaPackage mediapackage, String elementId) throws DistributionException, MediaPackageException { if (mediapackage == null) throw new MediaPackageException("Mediapackage must be specified"); if (elementId == null) throw new MediaPackageException("Element ID must be specified"); MediaPackageElement element = mediapackage.getElementById(elementId); if (element == null) throw new IllegalStateException("Mediapackage does not contain element " + elementId); if (!Track.TYPE.equals(element.getElementType())) { logger.info("Ignoring media package element {}", element); return null; } try { return serviceRegistry.createJob(JOB_TYPE, Operation.Distribute.toString(), Arrays.asList(MediaPackageParser.getAsXml(mediapackage), elementId)); } catch (ServiceRegistryException e) { throw new DistributionException("Unable to create a job", e); } } /** * Distributes the element to the distribution channel and returns a reference to the distributed version of the * element. * * @param job * the associated job * @param mediaPackageId * the mediapackage identifier * @param element * the element to distribute * @return the distributed element * @throws DistributionException * if distribution fails * @throws MediaPackageException * if the mediapackage is in an inconsistent state */ private MediaPackageElement distribute(Job job, String mediaPackageId, MediaPackage mediaPackage) throws DistributionException { if (mediaPackageId == null) throw new IllegalArgumentException("Mediapackage ID must be specified"); if (mediaPackage == null) throw new IllegalArgumentException("Mediapackage must be specified"); MediaPackageElement element = mediaPackage.getElementById(mediaPackageId); if (element == null) throw new IllegalArgumentException("Mediapackage element must be specified"); if (element.getIdentifier() == null) throw new IllegalArgumentException("Mediapackage element must have an identifier"); if (element.getMimeType().toString().matches("text/xml")) throw new IllegalArgumentException("Mediapackage element cannot be XML"); try { // create context strategy for distribution // MediaPackage mp = getMediaPackageFromId(mediaPackageId); YoutubeDistributionContextStrategy contextStrategy = new YoutubeDistributionContextStrategy( mediaPackage, workspace); // Initialise the YT service for distribution of the mediapackage element YouTubeService ytService = new YouTubeService(config.getClientId(), config.getDeveloperKey()); logger.debug("set youtube user credentails and authenticate"); ytService.setUserCredentials(config.getUserId(), config.getPassword()); if (!isDefaultChunked) ytService.setChunkedMediaUpload(MediaService.NO_CHUNKED_MEDIA_REQUEST); // generate the VideoEntry to upload VideoEntry newEntry = new VideoEntry(); YouTubeMediaGroup mg = newEntry.getOrCreateMediaGroup(); mg.setTitle(new MediaTitle()); String episodeName = contextStrategy.getEpisodeName(mediaPackageId); if (episodeName == null) { episodeName = "unknown"; } logger.debug("youtube episode name '{}' for media package element {}", episodeName, mediaPackageId); mg.getTitle().setPlainTextContent(episodeName); mg.addCategory(new MediaCategory(YouTubeNamespace.CATEGORY_SCHEME, config.getCategory())); mg.setKeywords(new MediaKeywords()); String[] keywords = contextStrategy.getEpisodeKeywords(mediaPackageId); logger.debug("youtube keywords '{}' for media element {}", Arrays.toString(keywords), mediaPackageId); Pattern regex = Pattern.compile("[^\\s\"']+|\"([^\"]*)\"|'([^']*)'"); if (keywords.length == 0) { mg.getKeywords().addKeyword("none"); } else { for (String word : keywords) { List<String> matchList = new ArrayList<String>(); Matcher regexMatcher = regex.matcher(word); while (regexMatcher.find()) { if (regexMatcher.group(1) != null) { // Add double-quoted string without the quotes matchList.add(regexMatcher.group(1)); } else if (regexMatcher.group(2) != null) { // Add single-quoted string without the quotes matchList.add(regexMatcher.group(2)); } else { // Add unquoted word matchList.add(regexMatcher.group()); } } for (String item : matchList) { mg.getKeywords().addKeyword(item); } } } // Add default keywords for (String item : config.getKeywords().split(", ")) { mg.getKeywords().addKeyword(item); } mg.setDescription(new MediaDescription()); String episodeDescription = contextStrategy.getEpisodeDescription(mediaPackageId); if (episodeDescription == null) { episodeDescription = "unknown"; } logger.debug("youtube episode description '{}' for media package element {}", episodeDescription, mediaPackageId); mg.getDescription().setPlainTextContent(episodeDescription); mg.setPrivate(config.isVideoPrivate()); // attach raw media file to video entry MediaFileSource ms = new MediaFileSource(workspace.get(element.getURI()), element.getMimeType().toString()); newEntry.setMediaSource(ms); // begin the upload to YouTube String videoUrl = null; VideoEntry uploadedEntry = null; try { uploadedEntry = ytService.insert(new URL(VIDEO_ENTRY_URL), newEntry); videoUrl = uploadedEntry.getSelfLink().getHref(); } catch (ServiceException e) { logger.error("Failed to upload to YouTube: {}", e.getMessage()); throw new DistributionException(e); } // monitor the publication process before returning while (true) { VideoEntry checkedEntry = ytService.getEntry(new URL(videoUrl), VideoEntry.class); // if entry is not a draft it has been successfully delivered if (!checkedEntry.isDraft()) break; // check the publication status YtPublicationState pubState = checkedEntry.getPublicationState(); if (pubState.getState() != YtPublicationState.State.PROCESSING) { logger.error("Video distribution to YouTube failed in state: {}", pubState.getDescription()); throw new DistributionException( "Video distribution to YouTube failed in state: " + pubState.getDescription()); } Thread.sleep(POLL_MILLISECONDS); } // insert the video into the proper yt playlist mapped from the context strategy boolean playlistExists = false; PlaylistLinkEntry playlist = null; String playlistName = contextStrategy.getContextName(mediaPackageId); String playlistDescription = contextStrategy.getContextDescription(mediaPackageId); if (playlistName == null) { playlistName = defaultPlaylist; } logger.debug("youtube playlist name '{}' for media package element {}", playlistName, mediaPackageId); logger.debug("youtube playlist description '{}' for media package element {}", playlistDescription, mediaPackageId); String feedUrl = PLAYLIST_URL.replaceAll("\\{username\\}", config.getUserId()); while (feedUrl != null) { PlaylistLinkFeed playlistFeed = ytService.getFeed(new URL(feedUrl), PlaylistLinkFeed.class); for (PlaylistLinkEntry entry : playlistFeed.getEntries()) { if (entry.getTitle().getPlainText().matches(playlistName)) { logger.info("Playlist {} exists", playlistName); playlist = entry; playlistExists = true; break; } } if (playlistExists || playlistFeed.getNextLink() == null) break; feedUrl = playlistFeed.getNextLink().getHref(); } // create playlist if it doesn't exist if (!playlistExists) { logger.info("Creating playlist {}", playlistName); playlist = new PlaylistLinkEntry(); playlist.setTitle(new PlainTextConstruct(playlistName)); playlist.setSummary(new PlainTextConstruct(playlistDescription)); playlist = ytService.insert(new URL(feedUrl), playlist); } // add video to the playlist String playlistUrl = playlist.getFeedUrl(); VideoEntry videoToAdd = ytService.getEntry(new URL(videoUrl), VideoEntry.class); PlaylistEntry playlistEntry = new PlaylistEntry(videoToAdd); ytService.insert(new URL(playlistUrl), playlistEntry); logger.info("Upload succeeded. URL of video: {}", videoUrl); return MediaPackageElementBuilderFactory.newInstance().newElementBuilder() .elementFromURI(new URI(uploadedEntry.getMediaGroup().getPlayer().getUrl()), TYPE, YOUTUBE); } catch (Exception e) { logger.warn("Error distributing {}, {}", element, e.getMessage()); if (e instanceof DistributionException) { throw (DistributionException) e; } else { throw new DistributionException(e); } } } /** * {@inheritDoc} * * @see org.opencastproject.distribution.api.DistributionService#retract(org.opencastproject.mediapackage.MediaPackage, * java.lang.String) */ public Job retract(MediaPackage mediaPackage, String elementId) throws DistributionException { if (mediaPackage == null) throw new IllegalArgumentException("Mediapackage must be specified"); if (elementId == null) throw new IllegalArgumentException("Element ID must be specified"); try { List<String> arguments = new ArrayList<String>(); arguments.add(MediaPackageParser.getAsXml(mediaPackage)); arguments.add(elementId); return serviceRegistry.createJob(JOB_TYPE, Operation.Retract.toString(), arguments); } catch (ServiceRegistryException e) { throw new DistributionException("Unable to create a job", e); } } /** * Retracts the mediapackage from YouTube. * * @param job * the associated job * @param mediapackageId * the mediapackage * @throws DistributionException * if retract did not work */ private void retract(Job job, String mediaPackageId, MediaPackage mediaPackage) throws DistributionException { logger.info("Trying to remove media from package: {}", mediaPackageId); try { // Initialise the YT service for distribution of the mediapackage element YouTubeService ytService = new YouTubeService(config.getClientId(), config.getDeveloperKey()); ytService.setUserCredentials(config.getUserId(), config.getPassword()); // create context strategy for distribution YoutubeDistributionContextStrategy contextStrategy = new YoutubeDistributionContextStrategy( mediaPackage, workspace); String episodeName = contextStrategy.getEpisodeName(mediaPackageId); if (episodeName == null) { logger.error("Unable to retract a recording with no episode name"); } else { VideoFeed videoFeed = ytService.getFeed( new URL(VIDEO_FEED_URL.replaceAll("\\{username\\}", config.getUserId())), VideoFeed.class); for (VideoEntry videoEntry : videoFeed.getEntries()) { logger.info("Video title: {}", videoEntry.getTitle().getPlainText()); if (videoEntry.getTitle().getPlainText().matches(episodeName)) { logger.info("Removing video with id: {}", videoEntry.getId()); videoEntry.delete(); } } } } catch (Exception e) { logger.error("Failure retracting YouTube media {}", e.getMessage()); throw new DistributionException(e); } } /** * {@inheritDoc} * * @see org.opencastproject.job.api.AbstractJobProducer#process(org.opencastproject.job.api.Job) */ @Override protected String process(Job job) throws Exception { Operation op = null; try { op = Operation.valueOf(job.getOperation()); List<String> arguments = job.getArguments(); MediaPackage mediapackage = MediaPackageParser.getFromXml(arguments.get(0)); String elementId = arguments.get(1); switch (op) { case Distribute: MediaPackageElement distributedElement = distribute(job, elementId, mediapackage); return (distributedElement == null) ? null : MediaPackageElementParser.getAsXml(distributedElement); case Retract: retract(job, elementId, mediapackage); return null; default: throw new IllegalStateException("Don't know how to handle operation '" + job.getOperation() + "'"); } } catch (IllegalArgumentException e) { throw new ServiceRegistryException("This service can't handle operations of type '" + op + "'"); } catch (IndexOutOfBoundsException e) { throw new ServiceRegistryException( "This argument list for operation '" + op + "' does not meet expectations"); } catch (Exception e) { throw new ServiceRegistryException("Error handling operation '" + op + "'"); } } /** * Callback for the OSGi environment to set the workflow service * * @param service * instance of the workflow service */ protected void setWorkflowService(WorkflowService service) { this.workflowService = service; } /** * Callback for the OSGi environment to set the workspace reference. * * @param workspace * the workspace */ protected void setWorkspace(Workspace workspace) { this.workspace = workspace; } /** * Callback for the OSGi environment to set the service registry reference. * * @param serviceRegistry * the service registry */ protected void setServiceRegistry(ServiceRegistry serviceRegistry) { this.serviceRegistry = serviceRegistry; } /** * {@inheritDoc} * * @see org.opencastproject.job.api.AbstractJobProducer#getServiceRegistry() */ @Override protected ServiceRegistry getServiceRegistry() { return serviceRegistry; } /** * Callback for setting the security service. * * @param securityService * the securityService to set */ public void setSecurityService(SecurityService securityService) { this.securityService = securityService; } /** * Callback for setting the user directory service. * * @param userDirectoryService * the userDirectoryService to set */ public void setUserDirectoryService(UserDirectoryService userDirectoryService) { this.userDirectoryService = userDirectoryService; } /** * Sets a reference to the organization directory service. * * @param organizationDirectory * the organization directory */ public void setOrganizationDirectoryService(OrganizationDirectoryService organizationDirectory) { this.organizationDirectoryService = organizationDirectory; } /** * {@inheritDoc} * * @see org.opencastproject.job.api.AbstractJobProducer#getSecurityService() */ @Override protected SecurityService getSecurityService() { return securityService; } /** * {@inheritDoc} * * @see org.opencastproject.job.api.AbstractJobProducer#getUserDirectoryService() */ @Override protected UserDirectoryService getUserDirectoryService() { return userDirectoryService; } /** * {@inheritDoc} * * @see org.opencastproject.job.api.AbstractJobProducer#getOrganizationDirectoryService() */ @Override protected OrganizationDirectoryService getOrganizationDirectoryService() { return organizationDirectoryService; } }