/************************************************************************************************** * Copyright (c) 2014 Dennis Fischer. * * All rights reserved. This program and the accompanying materials * * are made available under the terms of the GNU Public License v3.0+ * * which accompanies this distribution, and is available at * * http://www.gnu.org/licenses/gpl.html * * * * Contributors: Dennis Fischer * **************************************************************************************************/ package de.chaosfisch.google.youtube.upload.metadata; import com.google.common.base.Charsets; import com.google.common.base.Joiner; import com.google.common.base.Strings; import com.google.common.collect.Maps; import com.google.inject.Inject; import com.mashape.unirest.http.HttpResponse; import com.mashape.unirest.http.Unirest; import com.mashape.unirest.http.exceptions.UnirestException; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.core.util.QuickWriter; import com.thoughtworks.xstream.io.HierarchicalStreamWriter; import com.thoughtworks.xstream.io.xml.DomDriver; import com.thoughtworks.xstream.io.xml.PrettyPrintWriter; import de.chaosfisch.google.GDATAConfig; import de.chaosfisch.google.account.Account; import de.chaosfisch.google.account.IAccountService; import de.chaosfisch.google.atom.VideoEntry; import de.chaosfisch.google.atom.youtube.YoutubeAccessControl; import de.chaosfisch.google.http.PersistentCookieStore; import de.chaosfisch.google.youtube.upload.Upload; import de.chaosfisch.google.youtube.upload.metadata.permissions.*; import org.apache.http.client.CookieStore; import org.apache.http.client.HttpClient; import org.apache.http.client.config.RequestConfig; import org.apache.http.impl.client.BasicCookieStore; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.cookie.BasicClientCookie; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.Writer; import java.util.ArrayList; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; public class AbstractMetadataService implements IMetadataService { private static final Logger LOGGER = LoggerFactory.getLogger(AbstractMetadataService.class); private static final String METADATA_UPDATE_URL = "http://gdata.youtube.com/feeds/api/users/default/uploads"; private static final String VIDEO_EDIT_URL = "http://www.youtube.com/edit?o=U&ns=1&video_id=%s"; private static final int SC_OK = 200; private static final int MONETIZE_PARAMS_SIZE = 20; private static final char MODIFIED_SEPERATOR = ','; private static final int METADATA_PARAMS_SIZE = 40; private final IAccountService accountService; @Inject public AbstractMetadataService(final IAccountService accountService) { this.accountService = accountService; } @Override public String atomBuilder(final Upload upload) { // create atom xml metadata - create object first, then convert with // xstream final Metadata metadata = upload.getMetadata(); final VideoEntry videoEntry = new VideoEntry(); videoEntry.mediaGroup.category = new ArrayList<>(1); videoEntry.mediaGroup.category.add(metadata.getCategory().toCategory()); videoEntry.mediaGroup.license = metadata.getLicense().getMetaIdentifier(); videoEntry.mediaGroup.title = metadata.getTitle(); videoEntry.mediaGroup.description = metadata.getDescription(); videoEntry.mediaGroup.keywords = Joiner.on(TagParser.TAG_DELIMITER).skipNulls().join(TagParser.parse(metadata.getKeywords())); final Permissions permissions = upload.getPermissions(); if (Visibility.PRIVATE == permissions.getVisibility() || Visibility.SCHEDULED == permissions.getVisibility()) { videoEntry.mediaGroup.ytPrivate = new Object(); } videoEntry.accessControl.add(new YoutubeAccessControl("embed", PermissionStringConverter.convertBoolean(permissions.isEmbed()))); videoEntry.accessControl.add(new YoutubeAccessControl("rate", PermissionStringConverter.convertBoolean(permissions.isRate()))); videoEntry.accessControl.add(new YoutubeAccessControl("commentVote", PermissionStringConverter.convertBoolean(permissions.isCommentvote()))); videoEntry.accessControl.add(new YoutubeAccessControl("comment", PermissionStringConverter.convertInteger(permissions.getComment().ordinal()))); videoEntry.accessControl.add( new YoutubeAccessControl("list", PermissionStringConverter.convertBoolean(Visibility.PUBLIC == permissions.getVisibility()))); // convert metadata with xstream final XStream xStream = new XStream(new DomDriver(Charsets.UTF_8.name()) { @Override public HierarchicalStreamWriter createWriter(final Writer out) { return new PrettyPrintWriter(out) { boolean isCDATA; @Override public void startNode(final String name, @SuppressWarnings("rawtypes") final Class clazz) { super.startNode(name, clazz); isCDATA = "media:description".equals(name) || "media:keywords".equals(name) || "media:title".equals(name); } @Override protected void writeText(final QuickWriter writer, String text) { final String tmpText = Strings.nullToEmpty(text); text = "null".equals(tmpText) ? "" : tmpText; if (isCDATA) { writer.write("<![CDATA["); writer.write(text); writer.write("]]>"); } else { super.writeText(writer, text); } } }; } }); xStream.autodetectAnnotations(true); return String.format("<?xml version=\"1.0\" encoding=\"UTF-8\"?>%s", xStream.toXML(videoEntry)); } @Override public void updateMetaData(final String atomData, final String videoId, final Account account) throws MetaBadRequestException, MetaIOException { try { final HttpResponse<String> response = Unirest.put(String.format("%s/%s", METADATA_UPDATE_URL, videoId)) .header("GData-Version", GDATAConfig.GDATA_V2) .header("X-GData-Key", "key=" + GDATAConfig.DEVELOPER_KEY) .header("Content-Type", "application/atom+xml; charset=UTF-8;") .header("Authorization", accountService.getAuthentication(account).getHeader()) .body(atomData) .asString(); if (SC_OK != response.getCode()) { LOGGER.error("Metadata - invalid", response.getBody()); throw new MetaBadRequestException(atomData, response.getCode()); } } catch (final MetaBadRequestException e) { throw e; } catch (final Exception e) { throw new MetaIOException(e); } } @Override public void activateBrowserfeatures(final Upload upload) throws UnirestException { // Create a local instance of cookie store // Populate cookies if needed final CookieStore cookieStore = new BasicCookieStore(); for (final PersistentCookieStore.SerializableCookie serializableCookie : upload.getAccount().getSerializeableCookies()) { final BasicClientCookie cookie = new BasicClientCookie(serializableCookie.getCookie().getName(), serializableCookie.getCookie().getValue()); cookie.setDomain(serializableCookie.getCookie().getDomain()); cookieStore.addCookie(cookie); } final HttpClient client = HttpClientBuilder.create().useSystemProperties().setDefaultCookieStore(cookieStore).build(); Unirest.setHttpClient(client); final HttpResponse<String> response = Unirest.get(String.format(VIDEO_EDIT_URL, upload.getVideoid())).asString(); changeMetadata(response.getBody(), upload); final RequestConfig clientConfig = RequestConfig.custom().setConnectTimeout(600000).setSocketTimeout(600000).build(); Unirest.setHttpClient(HttpClientBuilder.create().setDefaultRequestConfig(clientConfig).build()); } private String extractor(final String input, final String search, final String end) { return String.format("%s", input.substring(input.indexOf(search) + search.length(), input.indexOf(end, input.indexOf(search) + search.length()))); } private String boolConverter(final boolean flag) { return flag ? "yes" : "no"; } private void changeMetadata(final String content, final Upload upload) { final Map<String, Object> params = new HashMap<>(METADATA_PARAMS_SIZE); params.putAll(getMetadataSocial(upload)); params.putAll(getMetadataMonetization(content, upload)); params.putAll(getMetadataMetadata(upload)); params.putAll(getMetadataPermissions(upload)); System.out.println(Joiner.on(MODIFIED_SEPERATOR).skipNulls().join(params.keySet())); params.put("modified_fields", Joiner.on(MODIFIED_SEPERATOR).skipNulls().join(params.keySet())); params.put("creator_share_feeds", "yes"); final String token = extractor(content, "var session_token = \"", "\""); params.put("session_token", token); params.put("action_edit_video", "1"); try { final HttpResponse<String> response = Unirest.post(String.format("https://www.youtube.com/metadata_ajax?video_id=%s", upload.getVideoid())) .fields(params) .asString(); LOGGER.info(response.getBody()); } catch (final Exception e) { LOGGER.warn("Metadata not set", e); } } private Map<String, Object> getMetadataPermissions(final Upload upload) { final Map<String, Object> params = Maps.newHashMapWithExpectedSize(7); final Permissions permissions = upload.getPermissions(); params.put("allow_comments", boolConverter(!(Comment.DENIED == permissions.getComment()))); params.put("allow_comments_detail", Comment.ALLOWED == permissions.getComment() ? "all" : "approval"); params.put("allow_comment_ratings", boolConverter(permissions.isCommentvote())); params.put("allow_ratings", boolConverter(permissions.isRate())); params.put("allow_embedding", boolConverter(permissions.isEmbed())); params.put("self_racy", boolConverter(permissions.isAgeRestricted())); params.put("allow_public_stats", boolConverter(permissions.isPublicStatsViewable())); params.put("threed_type", permissions.getThreedD().name().toLowerCase()); return params; } private Map<String, Object> getMetadataMetadata(final Upload upload) { final Map<String, Object> params = Maps.newHashMapWithExpectedSize(4); final Metadata metadata = upload.getMetadata(); params.put("title", metadata.getTitle()); params.put("description", Strings.nullToEmpty(metadata.getDescription())); params.put("keywords", Joiner.on(TagParser.TAG_DELIMITER).skipNulls().join(TagParser.parse(metadata.getKeywords(), true))); params.put("reuse", License.YOUTUBE == metadata.getLicense() ? "all_rights_reserved" : "creative_commons"); return params; } private Map<String, Object> getMetadataMonetization(final String content, final Upload upload) { final Map<String, Object> params = Maps.newHashMapWithExpectedSize(MONETIZE_PARAMS_SIZE); final Metadata metadata = upload.getMetadata(); final Monetization monetization = upload.getMonetization(); if (monetization.isClaim() && License.YOUTUBE == upload.getMetadata().getLicense()) { params.put("video_monetization_style", "ads"); if (!monetization.isPartner() || ClaimOption.MONETIZE == monetization.getClaimoption()) { params.put("claim_style", "ads"); params.put("enable_overlay_ads", boolConverter(monetization.isOverlay())); params.put("trueview_instream", boolConverter(monetization.isTrueview())); params.put("instream", boolConverter(monetization.isInstream())); params.put("long_ads_checkbox", boolConverter(monetization.isInstreamDefaults())); params.put("paid_product", boolConverter(monetization.isProduct())); params.put("allow_syndication", boolConverter(Syndication.GLOBAL == monetization.getSyndication())); } if (monetization.isPartner()) { params.put("claim_type", ClaimType.AUDIO_VISUAL == monetization.getClaimtype() ? "B" : ClaimType.VISUAL == monetization.getClaimtype() ? "V" : "A"); final String toFind = ClaimOption.MONETIZE == monetization.getClaimoption() ? "Monetize in all countries" : ClaimOption.TRACK == monetization .getClaimoption() ? "Track in all countries" : "Block in all countries"; final Pattern pattern = Pattern.compile( "<option\\s*value=\"([^\"]+?)\"\\s*(selected(=\"\")?)?\\sdata-is-monetized-policy=\"(true|false)\"\\s*>\\s*([^<]+?)\\s*</option>"); final Matcher matcher = pattern.matcher(content); String usagePolicy = null; int position = 0; while (matcher.find(position)) { position = matcher.end(); if (matcher.group(5).trim().equals(toFind)) { usagePolicy = matcher.group(1); } } params.put("usage_policy", usagePolicy); final String assetName = monetization.getAsset().name().toLowerCase(Locale.getDefault()); params.put("asset_type", assetName); params.put(assetName + "_custom_id", monetization.getCustomId().isEmpty() ? upload.getVideoid() : monetization.getCustomId()); params.put(assetName + "_notes", monetization.getNotes()); params.put(assetName + "_tms_id", monetization.getTmsid()); params.put(assetName + "_isan", monetization.getIsan()); params.put(assetName + "_eidr", monetization.getEidr()); if (Asset.TV != monetization.getAsset()) { // WEB + MOVIE ONLY params.put(assetName + "_title", !monetization.getTitle().isEmpty() ? monetization.getTitle() : metadata.getTitle()); params.put(assetName + "_description", monetization.getDescription()); } else { // TV ONLY params.put("show_title", monetization.getTitle()); params.put("episode_title", monetization.getTitleepisode()); params.put("season_nb", monetization.getSeasonNb()); params.put("episode_nb", monetization.getEpisodeNb()); } } } return params; } private Map<String, Object> getMetadataSocial(final Upload upload) { final Map<String, Object> params = new HashMap<>(3); final Permissions permissions = upload.getPermissions(); if (Visibility.PUBLIC == permissions.getVisibility() || Visibility.SCHEDULED == permissions.getVisibility()) { final Social social = upload.getSocial(); if (null != social.getMessage() && !social.getMessage().isEmpty()) { params.put("creator_share_custom_message", social.getMessage()); params.put("creator_share_facebook", boolConverter(social.isFacebook())); params.put("creator_share_twitter", boolConverter(social.isTwitter())); params.put("creator_share_gplus", boolConverter(social.isGplus())); } } return params; } }