commit 290420a94f30976407a017744bc4396a06e7f055 parent 79b11b8ffce028a359d04c4738af144bfe8afc51 Author: Kebigon <git@kebigon.xyz> Date: Sat, 25 Jan 2020 15:37:27 +0900 Rebase YuzuRSS on Spring MVC, ROME and JSON Feed Diffstat:
| M | .gitignore | | | 110 | +++++++++++++++++++++++++++++++++++++++++-------------------------------------- |
| D | .mvn/wrapper/maven-wrapper.properties | | | 1 | - |
| M | README.md | | | 55 | +++++++++++++++++++++++++++++-------------------------- |
| A | launchers/YuzuRSS - Package.launch | | | 17 | +++++++++++++++++ |
| A | launchers/YuzuRSS - Run.launch | | | 22 | ++++++++++++++++++++++ |
| D | mvnw | | | 286 | ------------------------------------------------------------------------------- |
| D | mvnw.cmd | | | 161 | ------------------------------------------------------------------------------- |
| M | pom.xml | | | 70 | +++++++++++++++++++++++++++++++++++++++------------------------------- |
| M | src/assembly/assembly.xml | | | 18 | ++++-------------- |
| D | src/main/external-resources/application.properties | | | 4 | ---- |
| D | src/main/external-resources/yuzurss.sh | | | 27 | --------------------------- |
| D | src/main/java/fr/lrgn/yuzurss/FeedClient.java | | | 63 | --------------------------------------------------------------- |
| D | src/main/java/fr/lrgn/yuzurss/FeedController.java | | | 28 | ---------------------------- |
| D | src/main/java/fr/lrgn/yuzurss/FeedEntry.java | | | 43 | ------------------------------------------- |
| D | src/main/java/fr/lrgn/yuzurss/FeedRequestBody.java | | | 25 | ------------------------- |
| D | src/main/java/fr/lrgn/yuzurss/YuzuRSSErrorAttributes.java | | | 23 | ----------------------- |
| D | src/main/java/fr/lrgn/yuzurss/YuzuRssApplication.java | | | 16 | ---------------- |
| D | src/main/java/fr/lrgn/yuzurss/exception/DateParseException.java | | | 13 | ------------- |
| D | src/main/java/fr/lrgn/yuzurss/exception/NoParserFoundException.java | | | 13 | ------------- |
| D | src/main/java/fr/lrgn/yuzurss/exception/YuzuRSSException.java | | | 16 | ---------------- |
| D | src/main/java/fr/lrgn/yuzurss/parser/AtomFeedParser.java | | | 67 | ------------------------------------------------------------------- |
| D | src/main/java/fr/lrgn/yuzurss/parser/FeedParser.java | | | 17 | ----------------- |
| D | src/main/java/fr/lrgn/yuzurss/parser/RDFFeedParser.java | | | 69 | --------------------------------------------------------------------- |
| D | src/main/java/fr/lrgn/yuzurss/parser/RSSFeedParser.java | | | 69 | --------------------------------------------------------------------- |
| A | src/main/java/xyz/kebigon/yuzurss/FeedClient.java | | | 43 | +++++++++++++++++++++++++++++++++++++++++++ |
| A | src/main/java/xyz/kebigon/yuzurss/FeedController.java | | | 52 | ++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | src/main/java/xyz/kebigon/yuzurss/FeedRequestBody.java | | | 13 | +++++++++++++ |
| A | src/main/java/xyz/kebigon/yuzurss/YuzuRSSApplication.java | | | 16 | ++++++++++++++++ |
| A | src/main/java/xyz/kebigon/yuzurss/json/Author.java | | | 14 | ++++++++++++++ |
| A | src/main/java/xyz/kebigon/yuzurss/json/Feed.java | | | 23 | +++++++++++++++++++++++ |
| A | src/main/java/xyz/kebigon/yuzurss/json/Item.java | | | 67 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | src/main/packaged-resources/cfg/application.properties | | | 7 | +++++++ |
| D | src/test/java/fr/lrgn/yuzurss/YuzuRssApplicationTests.java | | | 168 | ------------------------------------------------------------------------------- |
| A | src/test/java/xyz/kebigon/yuzurss/YuzuRSSApplicationTests.java | | | 14 | ++++++++++++++ |
| A | src/test/java/xyz/kebigon/yuzurss/json/ItemTests.java | | | 141 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | src/test/resources/bootstrap.xml | | | 314 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | src/test/resources/suumo.xml | | | 225 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | src/test/resources/youtube.xml | | | 697 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
38 files changed, 1794 insertions(+), 1233 deletions(-)
diff --git a/.gitignore b/.gitignore @@ -1,53 +1,7 @@ -######## -# Java # -######## +# +# Eclipse +# -# Compiled class file -*.class - -# Log file -*.log - -# BlueJ files -*.ctxt - -# Mobile Tools for Java (J2ME) -.mtj.tmp/ - -# Package Files # -*.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar - -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* - - -######### -# Maven # -######### - -target/ -pom.xml.tag -pom.xml.releaseBackup -pom.xml.versionsBackup -pom.xml.next -release.properties -dependency-reduced-pom.xml -buildNumber.properties -.mvn/timing.properties -.mvn/wrapper/maven-wrapper.jar - - -########### -# Eclipse # -########### - -.project .metadata bin/ tmp/ @@ -56,17 +10,16 @@ tmp/ *.swp *~.nib local.properties -.classpath .settings/ .loadpath .recommenders +# Eclipse Core +.project + # External tool builders .externalToolBuilders/ -# Locally stored "Eclipse launch configurations" -*.launch - # PyDev specific (Python IDE for Eclipse) *.pydevproject @@ -76,6 +29,9 @@ local.properties # CDT- autotools .autotools +# JDT-specific (Eclipse Java Development Tools) +.classpath + # Java annotation processor (APT) .factorypath @@ -99,8 +55,56 @@ local.properties # Annotation Processing .apt_generated/ +.apt_generated_test/ # Scala IDE specific (Scala & Java development for Eclipse) .cache-main .scala_dependencies .worksheet + + +# +# Java +# + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +/target/ + + +# +# Maven +# + +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +# https://github.com/takari/maven-wrapper#usage-without-binary-jar +.mvn/wrapper/maven-wrapper.jar diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties @@ -1 +0,0 @@ -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.5.4/apache-maven-3.5.4-bin.zip diff --git a/README.md b/README.md @@ -1,9 +1,9 @@ # YuzuRSS -RSS aggregator microservice based on Spring Webflux +Feed aggregator microservice based on Spring ## Usage -Just send a POST request to /feed/, with in the body of the request a JSON object with the below: +Just send a POST request, with in the body of the request a JSON object with the below: | Parameter | Description | | --------- | ------------------------------------------- | @@ -12,10 +12,10 @@ Just send a POST request to /feed/, with in the body of the request a JSON objec Example: - POST http://5.39.83.109:8091/feed/ + POST http://5.39.83.109:15866 { - "limit": 20, + "limit": 2, "urls": [ "https://www.youtube.com/feeds/videos.xml?user=epenser1", "https://www.youtube.com/feeds/videos.xml?user=scilabus", @@ -23,28 +23,31 @@ Example: ] } -The response will be a JSON list of entry, with the below: - -| Parameter | Description | -| --------- | ---------------------------------------------------------------------- | -| title | Title of the entry | -| link | Link to open the content of the entry | -| published | Time the entry has been published, format 2019-01-27T09:40:02.000+0000 | -| author | Name of the author of the entry | +The response will be a [JSON Feed](https://jsonfeed.org/version/1): Example: - [ - { - "title": "\"Métaphysique et Zététique\" Entretien Sceptique avec Sardoche", - "link": "https://www.youtube.com/watch?v=WxNQp0fDFg0", - "published": "2019-01-27T09:40:02.000+0000", - "author": "La Tronche en Biais" - }, - { - "title": "#06 L'odorat, partie 2 - Les sens humains - e-penser", - "link": "https://www.youtube.com/watch?v=VQjutS-MBUc", - "published": "2019-01-20T14:05:57.000+0000", - "author": "e-penser" - } - ] + { + "version": "https://jsonfeed.org/version/1", + "title": "YuzuRSS aggregated feed", + "items": [ + { + "id": "yt:video:C9bLMf6Q-qk", + "url": "https://invidio.us/watch?v=C9bLMf6Q-qk", + "title": "Pour en finir avec l'homéopathie - Tronche de Fake 4.8", + "author": { + "name": "La Tronche en Biais" + }, + "date_published": "2020-01-23T10:43:08.000Z" + }, + { + "id": "yt:video:hJe5MDMWOaU", + "url": "https://invidio.us/watch?v=hJe5MDMWOaU", + "title": "Les trous noirs (1/2) - 48 - e-penser", + "author": { + "name": "e-penser" + }, + "date_published": "2020-01-23T08:09:43.000Z" + } + ] + } diff --git a/launchers/YuzuRSS - Package.launch b/launchers/YuzuRSS - Package.launch @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<launchConfiguration type="org.eclipse.m2e.Maven2LaunchConfigurationType"> +<booleanAttribute key="M2_DEBUG_OUTPUT" value="false"/> +<stringAttribute key="M2_GOALS" value="clean package"/> +<booleanAttribute key="M2_NON_RECURSIVE" value="false"/> +<booleanAttribute key="M2_OFFLINE" value="false"/> +<stringAttribute key="M2_PROFILES" value=""/> +<listAttribute key="M2_PROPERTIES"/> +<stringAttribute key="M2_RUNTIME" value="EMBEDDED"/> +<booleanAttribute key="M2_SKIP_TESTS" value="false"/> +<intAttribute key="M2_THREADS" value="1"/> +<booleanAttribute key="M2_UPDATE_SNAPSHOTS" value="false"/> +<stringAttribute key="M2_USER_SETTINGS" value=""/> +<booleanAttribute key="M2_WORKSPACE_RESOLUTION" value="false"/> +<booleanAttribute key="org.eclipse.jdt.launching.ATTR_USE_CLASSPATH_ONLY_JAR" value="false"/> +<stringAttribute key="org.eclipse.jdt.launching.WORKING_DIRECTORY" value="${project_loc:yuzurss}"/> +</launchConfiguration> diff --git a/launchers/YuzuRSS - Run.launch b/launchers/YuzuRSS - Run.launch @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<launchConfiguration type="org.eclipse.jdt.launching.localJavaApplication"> +<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS"> +<listEntry value="/yuzurss/src/main/java/xyz/kebigon/yuzurss/YuzuRSSApplication.java"/> +</listAttribute> +<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES"> +<listEntry value="1"/> +</listAttribute> +<booleanAttribute key="org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE" value="true"/> +<booleanAttribute key="org.eclipse.jdt.launching.ATTR_USE_CLASSPATH_ONLY_JAR" value="false"/> +<listAttribute key="org.eclipse.jdt.launching.CLASSPATH"> +<listEntry value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <runtimeClasspathEntry containerPath="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8" path="1" type="4"/> "/> +<listEntry value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <runtimeClasspathEntry path="3" projectName="yuzurss" type="1"/> "/> +<listEntry value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <runtimeClasspathEntry internalArchive="/yuzurss/src/main/packaged-resources/cfg" path="3" type="2"/> "/> +<listEntry value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <runtimeClasspathEntry containerPath="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER" path="3" type="4"/> "/> +</listAttribute> +<stringAttribute key="org.eclipse.jdt.launching.CLASSPATH_PROVIDER" value="org.eclipse.m2e.launchconfig.classpathProvider"/> +<booleanAttribute key="org.eclipse.jdt.launching.DEFAULT_CLASSPATH" value="false"/> +<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="xyz.kebigon.yuzurss.YuzuRSSApplication"/> +<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="yuzurss"/> +<stringAttribute key="org.eclipse.jdt.launching.SOURCE_PATH_PROVIDER" value="org.eclipse.m2e.launchconfig.sourcepathProvider"/> +</launchConfiguration> diff --git a/mvnw b/mvnw @@ -1,286 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache 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.apache.org/licenses/LICENSE-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. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir -# -# Optional ENV vars -# ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files -# ---------------------------------------------------------------------------- - -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi - - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi - -fi - -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" - fi - fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` - fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi - -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? -fi - -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi - -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`which java`" - fi -fi - -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi - -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." -fi - -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { - - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi - - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break - fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` - fi - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" - fi -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; -fi - -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found .mvn/wrapper/maven-wrapper.jar" - fi -else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." - fi - jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" - while IFS="=" read key value; do - case "$key" in (wrapperUrl) jarUrl="$value"; break ;; - esac - done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Downloading from: $jarUrl" - fi - wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" - - if command -v wget > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found wget ... using wget" - fi - wget "$jarUrl" -O "$wrapperJarPath" - elif command -v curl > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found curl ... using curl" - fi - curl -o "$wrapperJarPath" "$jarUrl" - else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Falling back to using Java to download" - fi - javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" - if [ -e "$javaClass" ]; then - if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Compiling MavenWrapperDownloader.java ..." - fi - # Compiling the Java class - ("$JAVA_HOME/bin/javac" "$javaClass") - fi - if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - # Running the downloader - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Running MavenWrapperDownloader.java ..." - fi - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") - fi - fi - fi -fi -########################################################################################## -# End of extension -########################################################################################## - -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR -fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` -fi - -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd @@ -1,161 +0,0 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" -FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( - IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - echo Found %WRAPPER_JAR% -) else ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %DOWNLOAD_URL% - powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" - echo Finished downloading %WRAPPER_JAR% -) -@REM End of extension - -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause - -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% - -exit /B %ERROR_CODE% diff --git a/pom.xml b/pom.xml @@ -1,20 +1,22 @@ <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> + + <groupId>xyz.kebigon</groupId> + <artifactId>yuzurss</artifactId> + <version>0.1.0-SNAPSHOT</version> + <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> - <version>2.1.8.RELEASE</version> + <version>2.2.4.RELEASE</version> <relativePath /> <!-- lookup parent from repository --> </parent> - <groupId>fr.lrgn</groupId> - <artifactId>yuzurss</artifactId> - <version>0.0.1-SNAPSHOT</version> - <packaging>jar</packaging> + <name>YuzuRSS</name> - <description>RSS aggregator microservice based on Spring Webflux</description> + <description>RSS aggregator microservice based on Spring MVC</description> <properties> <java.version>1.8</java.version> @@ -23,44 +25,43 @@ <dependencies> <dependency> <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-starter-webflux</artifactId> + <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> - <dependency> - <groupId>org.json</groupId> - <artifactId>json</artifactId> - <version>20190722</version> - </dependency> + <!-- Caffeine Cache --> <dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> </dependency> - <!-- Avoid java.lang.NoClassDefFoundError: reactor/core/Disposable --> + <!-- ROME --> <dependency> - <groupId>io.projectreactor</groupId> - <artifactId>reactor-core</artifactId> + <groupId>com.rometools</groupId> + <artifactId>rome</artifactId> + <version>1.12.2</version> </dependency> + <!-- Project Lombok --> <dependency> - <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-starter-test</artifactId> - <scope>test</scope> - </dependency> - <dependency> - <groupId>io.projectreactor</groupId> - <artifactId>reactor-test</artifactId> - <scope>test</scope> + <groupId>org.projectlombok</groupId> + <artifactId>lombok</artifactId> + <scope>provided</scope> </dependency> + <dependency> - <groupId>com.squareup.okhttp3</groupId> - <artifactId>mockwebserver</artifactId> - <version>3.14.3</version> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> + <exclusions> + <exclusion> + <groupId>org.junit.vintage</groupId> + <artifactId>junit-vintage-engine</artifactId> + </exclusion> + </exclusions> </dependency> </dependencies> @@ -68,23 +69,30 @@ <plugins> <plugin> <artifactId>maven-assembly-plugin</artifactId> - <!-- Version managed by spring-boot-dependencies --> <configuration> <descriptors> <descriptor>src/assembly/assembly.xml</descriptor> </descriptors> </configuration> + <executions> + <execution> + <phase>package</phase> + <goals> + <goal>single</goal> + </goals> + </execution> + </executions> </plugin> <plugin> - <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <configuration> - <useSystemClassLoader>false</useSystemClassLoader> + <additionalClasspathElements> + <additionalClasspathElement>src/main/packaged-resources/cfg</additionalClasspathElement> + </additionalClasspathElements> </configuration> </plugin> </plugins> </build> - </project> diff --git a/src/assembly/assembly.xml b/src/assembly/assembly.xml @@ -7,18 +7,8 @@ </formats> <fileSets> <fileSet> - <directory>src/main/external-resources</directory> - <outputDirectory>bin</outputDirectory> - <includes> - <include>yuzurss.sh</include> - </includes> - </fileSet> - <fileSet> - <directory>src/main/external-resources</directory> - <outputDirectory>cfg</outputDirectory> - <excludes> - <exclude>yuzurss.sh</exclude> - </excludes> + <directory>src/main/packaged-resources</directory> + <outputDirectory>.</outputDirectory> </fileSet> <fileSet> <directory>.</directory> @@ -32,13 +22,13 @@ <dependencySet> <outputDirectory>ext</outputDirectory> <excludes> - <exclude>fr.lrgn:yuzurss</exclude> + <exclude>xyz.kebigon:yuzurss</exclude> </excludes> </dependencySet> <dependencySet> <outputDirectory>lib</outputDirectory> <includes> - <include>fr.lrgn:yuzurss</include> + <include>xyz.kebigon:yuzurss</include> </includes> </dependencySet> </dependencySets> diff --git a/src/main/external-resources/application.properties b/src/main/external-resources/application.properties @@ -1,3 +0,0 @@ -server.port=8091 -spring.cache.cache-names=feeds -spring.cache.caffeine.spec=maximumSize=100,expireAfterAccess=10m -\ No newline at end of file diff --git a/src/main/external-resources/yuzurss.sh b/src/main/external-resources/yuzurss.sh @@ -1,26 +0,0 @@ -#!/bin/sh - -SERVICE_NAME=yuzurss -SERVICE_RAM=32M -SERVICE_MAIN=fr.lrgn.yuzurss.YuzuRssApplication - -if screen -ls $SERVICE_NAME | grep -q $SERVICE_NAME -then - - echo "The service $SERVICE_NAME is already started." - -else - - HEAP="-Xms$SERVICE_RAM -Xmx$SERVICE_RAM" - ERROR="-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=../var/log/$SERVICE_NAME-$(date +%Y%m%d-%H%M%S).hprof -XX:ErrorFile=../var/log/$SERVICE_NAME-$(date +%Y%m%d-%H%M%S)-error.log" - - GC="-XX:+UseG1GC -XX:MaxGCPauseMillis=50 -XX:GCPauseIntervalMillis=500 -XX:+DisableExplicitGC" - GC_LOGS="-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:../var/log/$SERVICE_NAME-$(date +%Y%m%d-%H%M%S)-gc.log" - - CLASSPATH="-cp ../cfg:../ext/*:../lib/*" - - screen -dmS $SERVICE_NAME java -server $HEAP $ERROR $GC $GC_LOGS $CLASSPATH $SERVICE_MAIN - - echo "The service $SERVICE_NAME has been started." - -fi -\ No newline at end of file diff --git a/src/main/java/fr/lrgn/yuzurss/FeedClient.java b/src/main/java/fr/lrgn/yuzurss/FeedClient.java @@ -1,63 +0,0 @@ -package fr.lrgn.yuzurss; - -import java.net.URI; - -import org.json.JSONObject; -import org.json.XML; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.cache.annotation.Cacheable; -import org.springframework.stereotype.Component; -import org.springframework.web.reactive.function.client.WebClient; -import org.springframework.web.reactive.function.client.WebClient.RequestHeadersSpec; -import org.springframework.web.reactive.function.client.WebClient.ResponseSpec; - -import fr.lrgn.yuzurss.exception.NoParserFoundException; -import fr.lrgn.yuzurss.parser.AtomFeedParser; -import fr.lrgn.yuzurss.parser.FeedParser; -import fr.lrgn.yuzurss.parser.RDFFeedParser; -import fr.lrgn.yuzurss.parser.RSSFeedParser; -import reactor.core.publisher.Flux; - -@Component -public class FeedClient -{ - private final Logger log = LoggerFactory.getLogger(getClass()); - - private final FeedParser[] parsers = new FeedParser[] { new AtomFeedParser(), new RDFFeedParser(), new RSSFeedParser() }; - - @Cacheable("feeds") - public Flux<FeedEntry> getFeed(URI uri) - { - final WebClient client = WebClient.create(); - final RequestHeadersSpec<?> request = client.get().uri(uri); - final ResponseSpec response = request.retrieve(); - - log.info("Downloading {}", uri); - - return response.bodyToMono(String.class).flatMapMany(xml -> { - log.info("Downloaded {}", uri); - - final JSONObject root = XML.toJSONObject(xml); - - try - { - return getFeedParser(uri, root).parseFeed(root); - } - catch (final Throwable e) - { - log.info("Exception while parsing {}", uri, e); - return Flux.empty(); - } - }).cache(); - } - - private FeedParser getFeedParser(URI uri, JSONObject root) - { - for (final FeedParser parser : parsers) - if (parser.acceptFeed(root)) - return parser; - - throw new NoParserFoundException(uri); - } -} diff --git a/src/main/java/fr/lrgn/yuzurss/FeedController.java b/src/main/java/fr/lrgn/yuzurss/FeedController.java @@ -1,28 +0,0 @@ -package fr.lrgn.yuzurss; - -import java.net.URI; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.CrossOrigin; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import reactor.core.publisher.Flux; - -@RestController -@RequestMapping("/feed") -@CrossOrigin -public class FeedController -{ - @Autowired - private FeedClient client; - - @PostMapping() - public Flux<FeedEntry> getFeeds(@RequestBody FeedRequestBody body) - { - return Flux.fromIterable(body.getUrls()).flatMap(url -> client.getFeed(URI.create(url))).sort(FeedEntry.COMPARATOR) - .take(body.getLimit()); - } -} diff --git a/src/main/java/fr/lrgn/yuzurss/FeedEntry.java b/src/main/java/fr/lrgn/yuzurss/FeedEntry.java @@ -1,42 +0,0 @@ -package fr.lrgn.yuzurss; - -import java.util.Comparator; -import java.util.Date; - -public class FeedEntry -{ - public static final Comparator<FeedEntry> COMPARATOR = (FeedEntry o1, FeedEntry o2) -> o2.getPublished().compareTo(o1.getPublished()); - - private final String title; - private final String link; - private final Date published; - private final String author; - - public FeedEntry(String title, String link, Date published, String author) - { - this.title = title; - this.link = link; - this.published = published; - this.author = author; - } - - public String getTitle() - { - return title; - } - - public String getLink() - { - return link; - } - - public Date getPublished() - { - return published; - } - - public String getAuthor() - { - return author; - } -} -\ No newline at end of file diff --git a/src/main/java/fr/lrgn/yuzurss/FeedRequestBody.java b/src/main/java/fr/lrgn/yuzurss/FeedRequestBody.java @@ -1,25 +0,0 @@ -package fr.lrgn.yuzurss; - -import java.util.List; - -public class FeedRequestBody -{ - private final List<String> urls; - private final int limit; - - public FeedRequestBody(List<String> urls, int limit) - { - this.urls = urls; - this.limit = limit; - } - - public List<String> getUrls() - { - return urls; - } - - public int getLimit() - { - return limit; - } -} diff --git a/src/main/java/fr/lrgn/yuzurss/YuzuRSSErrorAttributes.java b/src/main/java/fr/lrgn/yuzurss/YuzuRSSErrorAttributes.java @@ -1,23 +0,0 @@ -package fr.lrgn.yuzurss; - -import java.util.Map; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.boot.web.reactive.error.DefaultErrorAttributes; -import org.springframework.stereotype.Component; -import org.springframework.web.reactive.function.server.ServerRequest; - -@Component -public class YuzuRSSErrorAttributes extends DefaultErrorAttributes -{ - private final Logger log = LoggerFactory.getLogger(getClass()); - - @Override - public Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) - { - log.error("Error while processsing request {}", request.uri(), getError(request)); - - return super.getErrorAttributes(request, includeStackTrace); - } -} diff --git a/src/main/java/fr/lrgn/yuzurss/YuzuRssApplication.java b/src/main/java/fr/lrgn/yuzurss/YuzuRssApplication.java @@ -1,15 +0,0 @@ -package fr.lrgn.yuzurss; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.cache.annotation.EnableCaching; - -@SpringBootApplication -@EnableCaching -public class YuzuRssApplication -{ - public static void main(String[] args) - { - SpringApplication.run(YuzuRssApplication.class, args); - } -} -\ No newline at end of file diff --git a/src/main/java/fr/lrgn/yuzurss/exception/DateParseException.java b/src/main/java/fr/lrgn/yuzurss/exception/DateParseException.java @@ -1,13 +0,0 @@ -package fr.lrgn.yuzurss.exception; - -import java.text.ParseException; - -public class DateParseException extends YuzuRSSException -{ - private static final long serialVersionUID = 1L; - - public DateParseException(String date, String format, ParseException cause) - { - super("Unable to parse date " + date + " with format " + format, cause); - } -} diff --git a/src/main/java/fr/lrgn/yuzurss/exception/NoParserFoundException.java b/src/main/java/fr/lrgn/yuzurss/exception/NoParserFoundException.java @@ -1,13 +0,0 @@ -package fr.lrgn.yuzurss.exception; - -import java.net.URI; - -public class NoParserFoundException extends YuzuRSSException -{ - private static final long serialVersionUID = 1L; - - public NoParserFoundException(URI uri) - { - super("No parser found for feed " + uri); - } -} diff --git a/src/main/java/fr/lrgn/yuzurss/exception/YuzuRSSException.java b/src/main/java/fr/lrgn/yuzurss/exception/YuzuRSSException.java @@ -1,16 +0,0 @@ -package fr.lrgn.yuzurss.exception; - -abstract class YuzuRSSException extends RuntimeException -{ - private static final long serialVersionUID = 1L; - - protected YuzuRSSException(String message) - { - super(message); - } - - protected YuzuRSSException(String message, Throwable cause) - { - super(message, cause); - } -} diff --git a/src/main/java/fr/lrgn/yuzurss/parser/AtomFeedParser.java b/src/main/java/fr/lrgn/yuzurss/parser/AtomFeedParser.java @@ -1,67 +0,0 @@ -package fr.lrgn.yuzurss.parser; - -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; - -import org.json.JSONObject; - -import fr.lrgn.yuzurss.FeedEntry; -import fr.lrgn.yuzurss.exception.DateParseException; -import reactor.core.publisher.Flux; - -public class AtomFeedParser extends FeedParser -{ - private static final String ATOM_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ssXXX"; // 2018-11-03T18:12:15+00:00 - - private static final ThreadLocal<SimpleDateFormat> atomDateFormat = new ThreadLocal<SimpleDateFormat>() - { - @Override - protected SimpleDateFormat initialValue() - { - return new SimpleDateFormat(ATOM_DATE_FORMAT); - }; - }; - - @Override - public boolean acceptFeed(JSONObject root) - { - return root.has("feed"); - } - - @Override - public Flux<FeedEntry> parseFeed(JSONObject root) - { - Flux<FeedEntry> entries = Flux.empty(); - final JSONObject feed = root.optJSONObject("feed"); - - if (feed != null) - { - for (final Object entry : feed.getJSONArray("entry")) - { - log.debug("Parsing entry {}", entry); - - final String author = ((JSONObject) entry).getJSONObject("author").getString("name"); - final String link = ((JSONObject) entry).getJSONObject("link").getString("href"); - final String title = ((JSONObject) entry).getString("title"); - final Date published = parseDate(((JSONObject) entry).getString("published")); - - entries = entries.mergeWith(Flux.just(new FeedEntry(title, link, published, author))); - } - } - - return entries; - } - - public Date parseDate(String date) - { - try - { - return atomDateFormat.get().parse(date); - } - catch (final ParseException ex) - { - throw new DateParseException(date, ATOM_DATE_FORMAT, ex); - } - } -} diff --git a/src/main/java/fr/lrgn/yuzurss/parser/FeedParser.java b/src/main/java/fr/lrgn/yuzurss/parser/FeedParser.java @@ -1,17 +0,0 @@ -package fr.lrgn.yuzurss.parser; - -import org.json.JSONObject; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import fr.lrgn.yuzurss.FeedEntry; -import reactor.core.publisher.Flux; - -public abstract class FeedParser -{ - protected final Logger log = LoggerFactory.getLogger(getClass()); - - public abstract boolean acceptFeed(JSONObject root); - - public abstract Flux<FeedEntry> parseFeed(JSONObject root); -} diff --git a/src/main/java/fr/lrgn/yuzurss/parser/RDFFeedParser.java b/src/main/java/fr/lrgn/yuzurss/parser/RDFFeedParser.java @@ -1,69 +0,0 @@ -package fr.lrgn.yuzurss.parser; - -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; - -import org.json.JSONObject; - -import fr.lrgn.yuzurss.FeedEntry; -import fr.lrgn.yuzurss.exception.DateParseException; -import reactor.core.publisher.Flux; - -public class RDFFeedParser extends FeedParser -{ - private static final String RDF_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ssXXX"; // 2018-11-03T18:12:15+00:00 - - private static final ThreadLocal<SimpleDateFormat> rdfDateFormat = new ThreadLocal<SimpleDateFormat>() - { - @Override - protected SimpleDateFormat initialValue() - { - return new SimpleDateFormat(RDF_DATE_FORMAT); - }; - }; - - @Override - public boolean acceptFeed(JSONObject root) - { - return root.has("rdf:RDF"); - } - - @Override - public Flux<FeedEntry> parseFeed(JSONObject root) - { - Flux<FeedEntry> entries = Flux.empty(); - final JSONObject document = root.getJSONObject("rdf:RDF"); - - if (document.has("item")) - { - final JSONObject channel = document.getJSONObject("channel"); - final String author = channel.getString("title"); - - for (final Object entry : document.getJSONArray("item")) - { - log.debug("Parsing entry {}", entry); - - final String link = ((JSONObject) entry).getString("link"); - final String title = ((JSONObject) entry).getString("title"); - final Date published = parseDate(((JSONObject) entry).getString("dc:date")); - - entries = entries.mergeWith(Flux.just(new FeedEntry(title, link, published, author))); - } - } - - return entries; - } - - private Date parseDate(String date) - { - try - { - return rdfDateFormat.get().parse(date); - } - catch (final ParseException ex) - { - throw new DateParseException(date, RDF_DATE_FORMAT, ex); - } - } -} diff --git a/src/main/java/fr/lrgn/yuzurss/parser/RSSFeedParser.java b/src/main/java/fr/lrgn/yuzurss/parser/RSSFeedParser.java @@ -1,69 +0,0 @@ -package fr.lrgn.yuzurss.parser; - -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; - -import org.json.JSONObject; - -import fr.lrgn.yuzurss.FeedEntry; -import fr.lrgn.yuzurss.exception.DateParseException; -import reactor.core.publisher.Flux; - -public class RSSFeedParser extends FeedParser -{ - private static final String RSS_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss Z"; // Sun, 09 Dec 2018 09:22:00 +0000 - - private static final ThreadLocal<SimpleDateFormat> rssDateFormat = new ThreadLocal<SimpleDateFormat>() - { - @Override - protected SimpleDateFormat initialValue() - { - return new SimpleDateFormat(RSS_DATE_FORMAT, Locale.ROOT); - }; - }; - - @Override - public boolean acceptFeed(JSONObject root) - { - return root.has("rss"); - } - - @Override - public Flux<FeedEntry> parseFeed(JSONObject root) - { - Flux<FeedEntry> entries = Flux.empty(); - final JSONObject channel = root.getJSONObject("rss").getJSONObject("channel"); - - if (channel.has("item")) - { - final String author = channel.getString("title"); - - for (final Object entry : channel.getJSONArray("item")) - { - log.debug("Parsing entry {}", entry); - - final String link = ((JSONObject) entry).getString("link"); - final String title = ((JSONObject) entry).getString("title"); - final Date published = parseDate(((JSONObject) entry).getString("pubDate")); - - entries = entries.mergeWith(Flux.just(new FeedEntry(title, link, published, author))); - } - } - - return entries; - } - - public Date parseDate(String date) - { - try - { - return rssDateFormat.get().parse(date); - } - catch (final ParseException ex) - { - throw new DateParseException(date, RSS_DATE_FORMAT, ex); - } - } -} diff --git a/src/main/java/xyz/kebigon/yuzurss/FeedClient.java b/src/main/java/xyz/kebigon/yuzurss/FeedClient.java @@ -0,0 +1,42 @@ +package xyz.kebigon.yuzurss; + +import java.io.IOException; +import java.util.Arrays; + +import org.springframework.cache.annotation.Cacheable; +import org.springframework.http.HttpMethod; +import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter; +import org.springframework.http.converter.feed.RssChannelHttpMessageConverter; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import com.rometools.rome.feed.synd.SyndFeed; +import com.rometools.rome.io.FeedException; +import com.rometools.rome.io.SyndFeedInput; +import com.rometools.rome.io.XmlReader; + +import lombok.extern.slf4j.Slf4j; + +@Service +@Slf4j +public class FeedClient +{ + private final RestTemplate restTemplate = new RestTemplate(Arrays.asList(new AtomFeedHttpMessageConverter(), new RssChannelHttpMessageConverter())); + private final SyndFeedInput input = new SyndFeedInput(); + + @Cacheable("feeds") + public SyndFeed getFeed(String url) + { + log.info("Downloading {}", url); + + return restTemplate.execute(url, HttpMethod.GET, null, response -> { + try + { + return input.build(new XmlReader(response.getBody())); + } catch (IllegalArgumentException | FeedException e) + { + throw new IOException(e); + } + }); + } +} +\ No newline at end of file diff --git a/src/main/java/xyz/kebigon/yuzurss/FeedController.java b/src/main/java/xyz/kebigon/yuzurss/FeedController.java @@ -0,0 +1,51 @@ +package xyz.kebigon.yuzurss; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import xyz.kebigon.yuzurss.json.Feed; +import xyz.kebigon.yuzurss.json.Item; + +@RestController +@CrossOrigin +public class FeedController +{ + @Autowired + private FeedClient client; + + @Value("${feeds.youtube.replaceByInvidious}") + private boolean replaceYoutubeByInvidious; + @Value("${invidious.url}") + private String invidiousUrl; + + @PostMapping + public Feed getFeeds(@RequestBody FeedRequestBody body) + { + List<Item> items = body.getUrls().parallelStream() // + .flatMap(this::getItems) // + .sorted() // + .collect(Collectors.toList()); + + if (body.getLimit() < items.size()) + items = items.subList(0, body.getLimit()); + + if (replaceYoutubeByInvidious) + items.parallelStream().filter(item -> item.getUrl().contains("https://www.youtube.com")) + .forEach(item -> item.setUrl(item.getUrl().replace("https://www.youtube.com", invidiousUrl))); + + return new Feed(items); + } + + private Stream<Item> getItems(String url) + { + return client.getFeed(url).getEntries().stream().map(entry -> new Item(entry)); + } +} +\ No newline at end of file diff --git a/src/main/java/xyz/kebigon/yuzurss/FeedRequestBody.java b/src/main/java/xyz/kebigon/yuzurss/FeedRequestBody.java @@ -0,0 +1,12 @@ +package xyz.kebigon.yuzurss; + +import java.util.List; + +import lombok.Data; + +@Data +public class FeedRequestBody +{ + private final List<String> urls; + private final int limit; +} +\ No newline at end of file diff --git a/src/main/java/xyz/kebigon/yuzurss/YuzuRSSApplication.java b/src/main/java/xyz/kebigon/yuzurss/YuzuRSSApplication.java @@ -0,0 +1,15 @@ +package xyz.kebigon.yuzurss; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; + +@EnableCaching +@SpringBootApplication +public class YuzuRSSApplication +{ + public static void main(String[] args) + { + SpringApplication.run(YuzuRSSApplication.class, args); + } +} +\ No newline at end of file diff --git a/src/main/java/xyz/kebigon/yuzurss/json/Author.java b/src/main/java/xyz/kebigon/yuzurss/json/Author.java @@ -0,0 +1,13 @@ +package xyz.kebigon.yuzurss.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; + +import lombok.Data; + +@Data +@JsonInclude(Include.NON_NULL) +public class Author +{ + private final String name; +} +\ No newline at end of file diff --git a/src/main/java/xyz/kebigon/yuzurss/json/Feed.java b/src/main/java/xyz/kebigon/yuzurss/json/Feed.java @@ -0,0 +1,22 @@ +package xyz.kebigon.yuzurss.json; + +import java.util.Collection; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; + +import lombok.Data; + +@Data +@JsonInclude(Include.NON_NULL) +public class Feed +{ + private final String version = "https://jsonfeed.org/version/1"; + private final String title = "YuzuRSS aggregated feed"; + private final Collection<Item> items; + + public Feed(Collection<Item> items) + { + this.items = items; + } +} +\ No newline at end of file diff --git a/src/main/java/xyz/kebigon/yuzurss/json/Item.java b/src/main/java/xyz/kebigon/yuzurss/json/Item.java @@ -0,0 +1,66 @@ +package xyz.kebigon.yuzurss.json; + +import java.util.Date; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonFormat.Shape; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.rometools.rome.feed.synd.SyndEntry; + +import lombok.Data; + +@Data +@JsonInclude(Include.NON_NULL) +public class Item implements Comparable<Item> +{ + public static final String DATE_FORMAT_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"; + public static final String DATE_FORMAT_TINEZONE = "UTC"; + + private final String id; + private String url; + private final String title; + + @JsonProperty("content_html") + private String contentHtml; + private String summary; + + @JsonProperty("date_published") + @JsonFormat(shape = Shape.STRING, pattern = DATE_FORMAT_PATTERN, timezone = DATE_FORMAT_TINEZONE) + private Date datePublished; + private Author author; + + public Item(SyndEntry entry) + { + this.id = entry.getUri(); + this.url = entry.getLink(); + this.title = sanitize(entry.getTitle()); + + if (!entry.getContents().isEmpty()) + contentHtml = entry.getContents().get(0).getValue(); + if (entry.getDescription() != null) + summary = sanitize(entry.getDescription().getValue()); + if (entry.getPublishedDate() != null) + this.datePublished = entry.getPublishedDate(); + if (!entry.getAuthor().isEmpty()) + this.author = new Author(entry.getAuthor()); + + if (contentHtml == null && summary != null) + contentHtml = summary; + } + + @Override + public int compareTo(Item other) + { + if (datePublished != null && other.datePublished != null) + return other.datePublished.compareTo(datePublished); + + return 0; + } + + public static String sanitize(String s) + { + return s.replace("\n", "").replace("\t", ""); + } +} +\ No newline at end of file diff --git a/src/main/packaged-resources/cfg/application.properties b/src/main/packaged-resources/cfg/application.properties @@ -0,0 +1,6 @@ +server.port=15866 +spring.cache.cache-names=feeds +spring.cache.caffeine.spec=maximumSize=100,expireAfterAccess=10m + +feeds.youtube.replaceByInvidious=true +invidious.url=https://invidio.us +\ No newline at end of file diff --git a/src/test/java/fr/lrgn/yuzurss/YuzuRssApplicationTests.java b/src/test/java/fr/lrgn/yuzurss/YuzuRssApplicationTests.java @@ -1,168 +0,0 @@ -package fr.lrgn.yuzurss; - -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.util.ArrayList; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.reactive.server.WebTestClient; -import org.springframework.web.reactive.function.BodyInserters; - -import okhttp3.mockwebserver.Dispatcher; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import okhttp3.mockwebserver.RecordedRequest; - -@RunWith(SpringRunner.class) -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -public class YuzuRssApplicationTests -{ - private static final String RSS_PATH = "/rss.xml"; - private static final String RSS_RESPONSE = "<rss><channel><title>rss_feed</title><item><link>rss_link1</link><title>rss_title1</title><pubDate>Sun, 09 Dec 2018 09:22:00 +0000</pubDate></item><item><link>rss_link2</link><title>rss_title2</title><pubDate>Fri, 19 Oct 2018 21:49:54 +0000</pubDate></item></channel></rss>"; - private static final String RDF_PATH = "/rdf.xml"; - private static final String RDF_RESPONSE = "<rdf:RDF><channel><title>rdf_feed</title></channel><item><link>rdf_link1</link><title>rdf_title1</title><dc:date>2019-01-06T14:18:00+09:00</dc:date></item><item><link>rdf_link2</link><title>rdf_title2</title><dc:date>2019-01-06T16:51:00+09:00</dc:date></item></rdf:RDF>"; - private static final String ATOM_PATH = "/atom.xml"; - private static final String ATOM_RESPONSE = "<feed><entry><author><name>atom_feed</name></author><link><href>atom_link1</href></link><title>atom_title1</title><published>2018-11-03T18:12:15+00:00</published></entry><entry><author><name>atom_feed</name></author><link><href>atom_link2</href></link><title>atom_title2</title><published>2018-10-30T18:12:15+00:00</published></entry></feed>"; - private static final String EMPTY_ATOM_PATH = "/empty_atom.xml"; - private static final String EMPTY_ATOM_RESPONSE = "<feed></feed>"; - private static final String EMPTY_RSS_PATH = "/empty_rss.xml"; - private static final String EMPTY_RSS_RESPONSE = "<rss><channel><title>rss_feed</title></channel></rss>"; - private static final String EMPTY_RDF_PATH = "/empty_rdf.xml"; - private static final String EMPTY_RDF_RESPONSE = "<rdf:RDF><channel><title>rdf_feed</title></channel></rdf:RDF>"; - - private static final String ATOM_RESULT = "[{\"title\":\"atom_title1\",\"link\":\"atom_link1\",\"published\":\"2018-11-03T18:12:15.000+0000\",\"author\":\"atom_feed\"},{\"title\":\"atom_title2\",\"link\":\"atom_link2\",\"published\":\"2018-10-30T18:12:15.000+0000\",\"author\":\"atom_feed\"}]"; - private static final String RSS_RESULT = "[{\"title\":\"rss_title1\",\"link\":\"rss_link1\",\"published\":\"2018-12-09T09:22:00.000+0000\",\"author\":\"rss_feed\"},{\"title\":\"rss_title2\",\"link\":\"rss_link2\",\"published\":\"2018-10-19T21:49:54.000+0000\",\"author\":\"rss_feed\"}]"; - private static final String RDF_RESULT = "[{\"title\":\"rdf_title2\",\"link\":\"rdf_link2\",\"published\":\"2019-01-06T07:51:00.000+0000\",\"author\":\"rdf_feed\"},{\"title\":\"rdf_title1\",\"link\":\"rdf_link1\",\"published\":\"2019-01-06T05:18:00.000+0000\",\"author\":\"rdf_feed\"}]"; - private static final String ATOM_RSS_RESULT = "[{\"title\":\"rss_title1\",\"link\":\"rss_link1\",\"published\":\"2018-12-09T09:22:00.000+0000\",\"author\":\"rss_feed\"},{\"title\":\"atom_title1\",\"link\":\"atom_link1\",\"published\":\"2018-11-03T18:12:15.000+0000\",\"author\":\"atom_feed\"},{\"title\":\"atom_title2\",\"link\":\"atom_link2\",\"published\":\"2018-10-30T18:12:15.000+0000\",\"author\":\"atom_feed\"},{\"title\":\"rss_title2\",\"link\":\"rss_link2\",\"published\":\"2018-10-19T21:49:54.000+0000\",\"author\":\"rss_feed\"}]"; - private static final String EMPTY_FEED_RESULT = "[]"; - - @Autowired - private WebTestClient webClient; - - private MockWebServer server; - - @Before - public void setup() throws IOException - { - server = new MockWebServer(); - server.setDispatcher(new Dispatcher() - { - @Override - public MockResponse dispatch(RecordedRequest request) throws InterruptedException - { - switch (request.getPath()) - { - case RSS_PATH: - return new MockResponse().setBody(RSS_RESPONSE); - case RDF_PATH: - return new MockResponse().setBody(RDF_RESPONSE); - case ATOM_PATH: - return new MockResponse().setBody(ATOM_RESPONSE); - case EMPTY_ATOM_PATH: - return new MockResponse().setBody(EMPTY_ATOM_RESPONSE); - case EMPTY_RSS_PATH: - return new MockResponse().setBody(EMPTY_RSS_RESPONSE); - case EMPTY_RDF_PATH: - return new MockResponse().setBody(EMPTY_RDF_RESPONSE); - } - return null; - } - }); - server.start(); - } - - @After - public void shutdown() throws IOException - { - server.shutdown(); - } - - @Test - public void contextLoads() - { - } - - @Test - public void testAtomFeed() throws UnsupportedEncodingException - { - final ArrayList<String> urls = new ArrayList<String>(); - urls.add("http://127.0.0.1:" + server.getPort() + "/atom.xml"); - final FeedRequestBody body = new FeedRequestBody(urls, 10); - - webClient.post().uri("/feed").body(BodyInserters.fromObject(body)).exchange().expectStatus().isOk().expectBody(String.class) - .isEqualTo(ATOM_RESULT); - } - - @Test - public void testRdfFeed() throws UnsupportedEncodingException - { - final ArrayList<String> urls = new ArrayList<String>(); - urls.add("http://127.0.0.1:" + server.getPort() + "/rdf.xml"); - final FeedRequestBody body = new FeedRequestBody(urls, 10); - - webClient.post().uri("/feed").body(BodyInserters.fromObject(body)).exchange().expectStatus().isOk().expectBody(String.class) - .isEqualTo(RDF_RESULT); - } - - @Test - public void testRssFeed() throws UnsupportedEncodingException - { - final ArrayList<String> urls = new ArrayList<String>(); - urls.add("http://127.0.0.1:" + server.getPort() + "/rss.xml"); - final FeedRequestBody body = new FeedRequestBody(urls, 10); - - webClient.post().uri("/feed").body(BodyInserters.fromObject(body)).exchange().expectStatus().isOk().expectBody(String.class) - .isEqualTo(RSS_RESULT); - } - - @Test - public void testAtomRSSFeed() throws UnsupportedEncodingException - { - final ArrayList<String> urls = new ArrayList<String>(); - urls.add("http://127.0.0.1:" + server.getPort() + "/atom.xml"); - urls.add("http://127.0.0.1:" + server.getPort() + "/rss.xml"); - final FeedRequestBody body = new FeedRequestBody(urls, 10); - - webClient.post().uri("/feed").body(BodyInserters.fromObject(body)).exchange().expectStatus().isOk().expectBody(String.class) - .isEqualTo(ATOM_RSS_RESULT); - } - - @Test - public void testEmptyAtomFeed() throws UnsupportedEncodingException - { - final ArrayList<String> urls = new ArrayList<String>(); - urls.add("http://127.0.0.1:" + server.getPort() + EMPTY_ATOM_PATH); - final FeedRequestBody body = new FeedRequestBody(urls, 10); - - webClient.post().uri("/feed").body(BodyInserters.fromObject(body)).exchange().expectStatus().isOk().expectBody(String.class) - .isEqualTo(EMPTY_FEED_RESULT); - } - - @Test - public void testEmptyRSSFeed() throws UnsupportedEncodingException - { - final ArrayList<String> urls = new ArrayList<String>(); - urls.add("http://127.0.0.1:" + server.getPort() + EMPTY_RSS_PATH); - final FeedRequestBody body = new FeedRequestBody(urls, 10); - - webClient.post().uri("/feed").body(BodyInserters.fromObject(body)).exchange().expectStatus().isOk().expectBody(String.class) - .isEqualTo(EMPTY_FEED_RESULT); - } - - @Test - public void testEmptyRDFFeed() throws UnsupportedEncodingException - { - final ArrayList<String> urls = new ArrayList<String>(); - urls.add("http://127.0.0.1:" + server.getPort() + EMPTY_RDF_PATH); - final FeedRequestBody body = new FeedRequestBody(urls, 10); - - webClient.post().uri("/feed").body(BodyInserters.fromObject(body)).exchange().expectStatus().isOk().expectBody(String.class) - .isEqualTo(EMPTY_FEED_RESULT); - } -} diff --git a/src/test/java/xyz/kebigon/yuzurss/YuzuRSSApplicationTests.java b/src/test/java/xyz/kebigon/yuzurss/YuzuRSSApplicationTests.java @@ -0,0 +1,13 @@ +package xyz.kebigon.yuzurss; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class YuzuRSSApplicationTests +{ + @Test + void contextLoads() + { + } +} +\ No newline at end of file diff --git a/src/test/java/xyz/kebigon/yuzurss/json/ItemTests.java b/src/test/java/xyz/kebigon/yuzurss/json/ItemTests.java @@ -0,0 +1,140 @@ +package xyz.kebigon.yuzurss.json; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.TimeZone; + +import org.junit.jupiter.api.Test; + +import com.rometools.rome.feed.synd.SyndFeed; +import com.rometools.rome.io.FeedException; +import com.rometools.rome.io.SyndFeedInput; +import com.rometools.rome.io.XmlReader; + +public class ItemTests +{ + private final SimpleDateFormat dateFormat = new SimpleDateFormat(Item.DATE_FORMAT_PATTERN); + + public ItemTests() + { + dateFormat.setTimeZone(TimeZone.getTimeZone(Item.DATE_FORMAT_TINEZONE)); + } + + @Test + public void testBootstrapFeed() throws IllegalArgumentException, FeedException, IOException + { + final SyndFeedInput input = new SyndFeedInput(); + final SyndFeed feed = input.build(new XmlReader(getClass().getResource("/bootstrap.xml"))); + + final Author author = new Author("{\"twitter\"=>\"getbootstrap\"}"); + + final Item item0 = new Item(feed.getEntries().get(0)); + assertEquals("https://blog.getbootstrap.com/2019/12/14/bootstrap-icons-alpha2", item0.getId()); + assertEquals("https://blog.getbootstrap.com/2019/12/14/bootstrap-icons-alpha2/", item0.getUrl()); + assertEquals("Bootstrap Icons Alpha 2", item0.getTitle()); + assertEquals( + "<p>There’s a brand new update of Bootstrap Icons today with our second alpha release! We’ve updated nearly 20 icons and added over 100 new icons since our last release just a few weeks ago.</p>\n\n<h2 id=\"new-icons\">New icons</h2>\n\n<p>With over 120 new and updated icons, this is likely going to be our largest update before we our first stable release. We have some renamed icons, fixed bugs, new tools icons, new typography icons, tons of new arrows, and so much more.</p>\n\n<p><a href=\"https://icons.getbootstrap.com/\"><img src=\"/assets/img/2019/12/bootstrap-icons-alpha2-new.png\" alt=\"New icons in Alpha 2\" /></a></p>\n\n<h2 id=\"highlights\">Highlights</h2>\n\n<p>Here’s a summary of what’s been fixed, updated, or renamed in this release. For a full summary of what’s new, head to the <a href=\"https://github.com/twbs/icons/releases/tag/v1.0.0-alpha2\">GitHub release</a> or <a href=\"https://github.com/twbs/icons/pull/78\">Alpha 2 pull request</a>.</p>\n\n<ul>\n <li><strong>Fixed:</strong>\n <ul>\n <li>Bootstrap icon stroke now 1px instead of 1.5px</li>\n <li>tv-fill icon no longer has graphical glitch</li>\n <li>circle-slash icon strokes now connect</li>\n <li>trash icons now use a single shape</li>\n <li>trash-fill icon no longer has a gap between lid and bin</li>\n <li>layout-split no longer has space between vertical divide</li>\n </ul>\n </li>\n <li><strong>Updated:</strong>\n <ul>\n <li>blockquote icons now feature more legible quotation marks</li>\n <li>command icon now 1px smaller, no longer sitting on half pixel</li>\n <li>gear icons now have rounded corners</li>\n <li>eye is now outline by default (use new eye-fill variant if needed)</li>\n <li>redrew sound waves on volume icons</li>\n <li>corrected (reversed) the direction of backspace icon</li>\n </ul>\n </li>\n <li><strong>Renamed:</strong>\n <ul>\n <li>changed microphone to mic</li>\n <li>existing expand/contract icons are now angle-expand and angle-contract</li>\n </ul>\n </li>\n</ul>\n\n<h2 id=\"get-em\">Get ‘em</h2>\n\n<p>Alpha 2 has been published to GitHub and npm (package name <code class=\"language-plaintext highlighter-rouge\">bootstrap-icons</code>). Get your hands on it <a href=\"https://github.com/twbs/icons/releases\">from GitHub</a>, by updating to <code class=\"language-plaintext highlighter-rouge\">v1.0.0-alpha2</code>, or by snagging the <a href=\"https://www.figma.com/file/0xfDVFogWu6g15bVOvBzxS/Bootstrap-Icons-v1.0.0-alpha2\">icons from Figma</a>.</p>\n\n<p><3,<br /></p>\n\n<p><a href=\"https://github.com/mdo\">@mdo</a> & <a href=\"https://github.com/twbs\">team</a></p>", + item0.getContentHtml()); + assertEquals( + "There’s a brand new update of Bootstrap Icons today with our second alpha release! We’ve updated nearly 20 icons and added over 100 new icons since our last release just a few weeks ago.", + item0.getSummary()); + assertEquals("2019-12-14T00:00:00.000Z", dateFormat.format(item0.getDatePublished())); + assertEquals(author, item0.getAuthor()); + + final Item item1 = new Item(feed.getEntries().get(1)); + assertEquals("https://blog.getbootstrap.com/2019/11/28/bootstrap-4-4-1", item1.getId()); + assertEquals("https://blog.getbootstrap.com/2019/11/28/bootstrap-4-4-1/", item1.getUrl()); + assertEquals("Bootstrap 4.4.1", item1.getTitle()); + assertEquals( + "<p>Today we’re shipping <a href=\"https://github.com/twbs/bootstrap/releases/tag/v4.4.1\">Bootstrap v4.4.1</a>!</p>\n\n<p>In <a href=\"/2019/11/26/bootstrap-4-4-0/\">v4.4.0</a>, we added <code class=\"language-plaintext highlighter-rouge\">add()</code> and <code class=\"language-plaintext highlighter-rouge\">subtract()</code> functions to avoid errors when using zero values in CSS’s built in <code class=\"language-plaintext highlighter-rouge\">calc()</code> function. While these functions work as expected with our build system, which is based on <code class=\"language-plaintext highlighter-rouge\">node-sass</code>, some alert developers noticed that <a href=\"https://github.com/twbs/bootstrap/issues/29743\">things broke when using another Sass compiler</a> like Dart Sass or Ruby Sass. To resolve this issue, we’ve tweaked these functions a bit to output what we would expect.</p>\n\n<p>Lastly, we also added a <a href=\"https://github.com/twbs/bootstrap/pull/29762\">theming fix</a> for some custom forms in a disabled fieldset.</p>\n\n<p><3,<br /></p>\n\n<p><a href=\"https://github.com/mdo\">@mdo</a> & <a href=\"https://github.com/twbs\">team</a></p>", + item1.getContentHtml()); + assertEquals("Today we’re shipping Bootstrap v4.4.1!", item1.getSummary()); + assertEquals("2019-11-28T00:00:00.000Z", dateFormat.format(item1.getDatePublished())); + assertEquals(author, item1.getAuthor()); + + final Item item2 = new Item(feed.getEntries().get(2)); + assertEquals("https://blog.getbootstrap.com/2019/11/26/bootstrap-4-4-0", item2.getId()); + assertEquals("https://blog.getbootstrap.com/2019/11/26/bootstrap-4-4-0/", item2.getUrl()); + assertEquals("Bootstrap 4.4.0", item2.getTitle()); + assertEquals( + "<p>Bootstrap 4 has a new update with a handful of feature changes. We’ve had quite the lengthy pull request to add responsive containers—big thanks to the developers who contribute to Bootstrap for sticking with it and helping us along the way. Nearly all new features will be carried forward into Bootstrap 5, so feel free to start using them now.</p>\n\n<h2 id=\"highlights\">Highlights</h2>\n\n<p>Here’s what you need to know about v4.4.0. Remember that with every minor and major release of Bootstrap, we ship a new URL for our hosted docs to ensure URLs continue to work.</p>\n\n<ul>\n <li><strong>New responsive containers!</strong> Over a year in the making, fluid up to a particular breakpoint, available for all responsive tiers.</li>\n <li><strong>New responsive <code class=\"language-plaintext highlighter-rouge\">.row-cols</code> classes</strong> for quickly specifying the number of columns across breakpoints. This one is huge for those of you who have asked for responsive card decks.</li>\n <li><strong>New <code class=\"language-plaintext highlighter-rouge\">escape-svg()</code> function</strong> for simplifying our embedded <code class=\"language-plaintext highlighter-rouge\">background-image</code> SVGs for forms and more.</li>\n <li><strong>New <code class=\"language-plaintext highlighter-rouge\">add()</code> and <code class=\"language-plaintext highlighter-rouge\">subtract()</code> functions</strong> for avoiding errors and zero values from CSS’s built in <code class=\"language-plaintext highlighter-rouge\">calc</code> feature.</li>\n <li><strong>New <code class=\"language-plaintext highlighter-rouge\">make-col-auto()</code> mixin</strong> to make our <code class=\"language-plaintext highlighter-rouge\">.col-auto</code> class available with custom HTML.</li>\n <li>Fixed an issue with Microsoft Edge not picking up <code class=\"language-plaintext highlighter-rouge\">:disabled</code> styles by moving selectors to <code class=\"language-plaintext highlighter-rouge\">[disabled]</code>.</li>\n <li><strong>Deprecated:</strong> <code class=\"language-plaintext highlighter-rouge\">bg-variant()</code>, <code class=\"language-plaintext highlighter-rouge\">nav-divider()</code>, and <code class=\"language-plaintext highlighter-rouge\">form-control-focus()</code> mixins are now deprecated as they’re going away in v5.</li>\n <li>Updated our spacing and alignment for modal footer elements like buttons to automatically wrap when space is constrained.</li>\n <li>More flexible form control validation styles thanks to fewer chained selectors. Also updated the <code class=\"language-plaintext highlighter-rouge\">:invalid</code> validation icon to be an alert instead of an <code class=\"language-plaintext highlighter-rouge\">&times;</code> to avoid confusion with browser functionality for clearing the form field value.</li>\n <li>Fixed a couple dozen CSS and JS bugs.</li>\n <li>Moved to GitHub Actions for CI/CD! Expect more updates to our CI setup over time here while Actions evolves.</li>\n <li>Updated documentation to fix links and typos, improved landmarks for secondary navigation, and a new security doc for guidelines on reporting potential vulnerabilities.</li>\n</ul>\n\n<p>We’ve shipped a lot more in this release, so be sure to check out the <a href=\"https://github.com/twbs/bootstrap/issues?q=project%3Atwbs%2Fbootstrap%2F18+is%3Aclosed+sort%3Aupdated-desc\">v4.4.0 ship list of closed issues and merged pull requests</a> for more details.</p>\n\n<p><a href=\"https://getbootstrap.com/docs/4.4/\">Head to to the v4.4.0 docs</a> to see the latest in action. The full release has been published to npm and will soon appear on the BootstrapCDN and Rubygems.</p>\n\n<h2 id=\"support-the-team\">Support the team</h2>\n\n<p>Visit our <a href=\"https://opencollective.com/bootstrap\">Open Collective page</a> or our <a href=\"https://github.com/orgs/twbs/people\">team members</a>’ GitHub profiles to help support the maintainers contributing to Bootstrap.</p>\n\n<p><3,<br /></p>\n\n<p><a href=\"https://github.com/mdo\">@mdo</a> & <a href=\"https://github.com/twbs\">team</a></p>", + item2.getContentHtml()); + assertEquals( + "Bootstrap 4 has a new update with a handful of feature changes. We’ve had quite the lengthy pull request to add responsive containers—big thanks to the developers who contribute to Bootstrap for sticking with it and helping us along the way. Nearly all new features will be carried forward into Bootstrap 5, so feel free to start using them now.", + item2.getSummary()); + assertEquals("2019-11-26T00:00:00.000Z", dateFormat.format(item2.getDatePublished())); + assertEquals(author, item2.getAuthor()); + } + + @Test + public void testSuumoFeed() throws IllegalArgumentException, FeedException, IOException + { + final SyndFeedInput input = new SyndFeedInput(); + final SyndFeed feed = input.build(new XmlReader(getClass().getResource("/suumo.xml"))); + + final Item item0 = new Item(feed.getEntries().get(0)); + assertEquals("https://suumo.jp/jj/bukken/shosai/JJ010FJ100/?ar=030&bs=021&nc=93385930&ta=12&sc=12234", item0.getId()); + assertEquals("https://suumo.jp/jj/bukken/shosai/JJ010FJ100/?ar=030&bs=021&nc=93385930&ta=12&sc=12234", item0.getUrl()); + assertEquals("物件名:自然の中に佇む、南房総の家", item0.getTitle()); + assertEquals("千葉県南房総市山田990万円JR内房線岩井623m²(登記)60.45m²(登記)1LDK1998年10月", item0.getContentHtml()); + assertEquals("千葉県南房総市山田990万円JR内房線岩井623m²(登記)60.45m²(登記)1LDK1998年10月", item0.getSummary()); + assertEquals(null, item0.getDatePublished()); + assertEquals(null, item0.getAuthor()); + + final Item item1 = new Item(feed.getEntries().get(1)); + assertEquals("https://suumo.jp/jj/bukken/shosai/JJ010FJ100/?ar=030&bs=021&nc=93373816&ta=11&sc=11242", item1.getId()); + assertEquals("https://suumo.jp/jj/bukken/shosai/JJ010FJ100/?ar=030&bs=021&nc=93373816&ta=11&sc=11242", item1.getUrl()); + assertEquals("物件名:大字高萩(武蔵高萩駅) 1180万円", item1.getTitle()); + assertEquals("埼玉県日高市大字高萩1180万円JR川越線武蔵高萩徒歩9分110.18m²(登記)87.58m²(登記)4LDK1980年2月", item1.getContentHtml()); + assertEquals("埼玉県日高市大字高萩1180万円JR川越線武蔵高萩徒歩9分110.18m²(登記)87.58m²(登記)4LDK1980年2月", item1.getSummary()); + assertEquals(null, item1.getDatePublished()); + assertEquals(null, item1.getAuthor()); + + final Item item2 = new Item(feed.getEntries().get(2)); + assertEquals("https://suumo.jp/jj/bukken/shosai/JJ010FJ100/?ar=030&bs=021&nc=93403901&ta=10&sc=10201", item2.getId()); + assertEquals("https://suumo.jp/jj/bukken/shosai/JJ010FJ100/?ar=030&bs=021&nc=93403901&ta=10&sc=10201", item2.getUrl()); + assertEquals("物件名:前橋市上細井町 中古戸建", item2.getTitle()); + assertEquals("群馬県前橋市上細井町1750万円上毛電鉄三俣徒歩38分202.5m²110.13m²4LDK2010年9月", item2.getContentHtml()); + assertEquals("群馬県前橋市上細井町1750万円上毛電鉄三俣徒歩38分202.5m²110.13m²4LDK2010年9月", item2.getSummary()); + assertEquals(null, item2.getDatePublished()); + assertEquals(null, item2.getAuthor()); + } + + @Test + public void testYoutubeFeed() throws IllegalArgumentException, FeedException, IOException + { + final SyndFeedInput input = new SyndFeedInput(); + final SyndFeed feed = input.build(new XmlReader(getClass().getResource("/youtube.xml"))); + + final Author author = new Author("e-penser"); + + final Item item0 = new Item(feed.getEntries().get(0)); + assertEquals("yt:video:hJe5MDMWOaU", item0.getId()); + assertEquals("https://www.youtube.com/watch?v=hJe5MDMWOaU", item0.getUrl()); + assertEquals("Les trous noirs (1/2) - 48 - e-penser", item0.getTitle()); + assertEquals(null, item0.getContentHtml()); + assertEquals(null, item0.getSummary()); + assertEquals("2020-01-23T08:09:43.000Z", dateFormat.format(item0.getDatePublished())); + assertEquals(author, item0.getAuthor()); + + final Item item1 = new Item(feed.getEntries().get(1)); + assertEquals("yt:video:Sk7Ia2Lsuak", item1.getId()); + assertEquals("https://www.youtube.com/watch?v=Sk7Ia2Lsuak", item1.getUrl()); + assertEquals("La migraine est une horreur - 47 - e-penser", item1.getTitle()); + assertEquals(null, item1.getContentHtml()); + assertEquals(null, item1.getSummary()); + assertEquals("2020-01-09T15:51:58.000Z", dateFormat.format(item1.getDatePublished())); + assertEquals(author, item1.getAuthor()); + + final Item item2 = new Item(feed.getEntries().get(2)); + assertEquals("yt:video:1Bn50keR6UY", item2.getId()); + assertEquals("https://www.youtube.com/watch?v=1Bn50keR6UY", item2.getUrl()); + assertEquals("Le mathématicien nul de l'Indiana - Flash 09 - e-penser", item2.getTitle()); + assertEquals(null, item2.getContentHtml()); + assertEquals(null, item2.getSummary()); + assertEquals("2019-12-27T17:00:25.000Z", dateFormat.format(item2.getDatePublished())); + assertEquals(author, item2.getAuthor()); + } +} +\ No newline at end of file diff --git a/src/test/resources/bootstrap.xml b/src/test/resources/bootstrap.xml @@ -0,0 +1,313 @@ +<?xml version="1.0" encoding="UTF-8"?> +<feed xmlns="http://www.w3.org/2005/Atom"><generator uri="https://jekyllrb.com/" version="3.8.5">Jekyll</generator><link href="https://blog.getbootstrap.com/feed.xml" rel="self" type="application/atom+xml"/><link href="https://blog.getbootstrap.com/" rel="alternate" type="text/html"/><updated>2020-01-21T07:53:09+00:00</updated><id>https://blog.getbootstrap.com/feed.xml</id><title type="html">Bootstrap Blog</title><subtitle>Official blog for the Bootstrap framework.</subtitle><author><name>{"twitter"=>"getbootstrap"}</name></author><entry><title type="html">Bootstrap Icons Alpha 2</title><link href="https://blog.getbootstrap.com/2019/12/14/bootstrap-icons-alpha2/" rel="alternate" type="text/html" title="Bootstrap Icons Alpha 2"/><published>2019-12-14T00:00:00+00:00</published><updated>2019-12-14T00:00:00+00:00</updated><id>https://blog.getbootstrap.com/2019/12/14/bootstrap-icons-alpha2</id><content type="html" xml:base="https://blog.getbootstrap.com/2019/12/14/bootstrap-icons-alpha2/"><p>There’s a brand new update of Bootstrap Icons today with our second alpha release! We’ve updated nearly 20 icons and added over 100 new icons since our last release just a few weeks ago.</p> + +<h2 id="new-icons">New icons</h2> + +<p>With over 120 new and updated icons, this is likely going to be our largest update before we our first stable release. We have some renamed icons, fixed bugs, new tools icons, new typography icons, tons of new arrows, and so much more.</p> + +<p><a href="https://icons.getbootstrap.com/"><img src="/assets/img/2019/12/bootstrap-icons-alpha2-new.png" alt="New icons in Alpha 2" /></a></p> + +<h2 id="highlights">Highlights</h2> + +<p>Here’s a summary of what’s been fixed, updated, or renamed in this release. For a full summary of what’s new, head to the <a href="https://github.com/twbs/icons/releases/tag/v1.0.0-alpha2">GitHub release</a> or <a href="https://github.com/twbs/icons/pull/78">Alpha 2 pull request</a>.</p> + +<ul> + <li><strong>Fixed:</strong> + <ul> + <li>Bootstrap icon stroke now 1px instead of 1.5px</li> + <li>tv-fill icon no longer has graphical glitch</li> + <li>circle-slash icon strokes now connect</li> + <li>trash icons now use a single shape</li> + <li>trash-fill icon no longer has a gap between lid and bin</li> + <li>layout-split no longer has space between vertical divide</li> + </ul> + </li> + <li><strong>Updated:</strong> + <ul> + <li>blockquote icons now feature more legible quotation marks</li> + <li>command icon now 1px smaller, no longer sitting on half pixel</li> + <li>gear icons now have rounded corners</li> + <li>eye is now outline by default (use new eye-fill variant if needed)</li> + <li>redrew sound waves on volume icons</li> + <li>corrected (reversed) the direction of backspace icon</li> + </ul> + </li> + <li><strong>Renamed:</strong> + <ul> + <li>changed microphone to mic</li> + <li>existing expand/contract icons are now angle-expand and angle-contract</li> + </ul> + </li> +</ul> + +<h2 id="get-em">Get ‘em</h2> + +<p>Alpha 2 has been published to GitHub and npm (package name <code class="language-plaintext highlighter-rouge">bootstrap-icons</code>). Get your hands on it <a href="https://github.com/twbs/icons/releases">from GitHub</a>, by updating to <code class="language-plaintext highlighter-rouge">v1.0.0-alpha2</code>, or by snagging the <a href="https://www.figma.com/file/0xfDVFogWu6g15bVOvBzxS/Bootstrap-Icons-v1.0.0-alpha2">icons from Figma</a>.</p> + +<p>&lt;3,<br /></p> + +<p><a href="https://github.com/mdo">@mdo</a> &amp; <a href="https://github.com/twbs">team</a></p></content><author><name>{"twitter"=>"getbootstrap"}</name></author><summary type="html">There’s a brand new update of Bootstrap Icons today with our second alpha release! We’ve updated nearly 20 icons and added over 100 new icons since our last release just a few weeks ago.</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.getbootstrap.com/assets/img/bootstrap-social.png"/><media:content medium="image" url="https://blog.getbootstrap.com/assets/img/bootstrap-social.png" xmlns:media="http://search.yahoo.com/mrss/"/></entry><entry><title type="html">Bootstrap 4.4.1</title><link href="https://blog.getbootstrap.com/2019/11/28/bootstrap-4-4-1/" rel="alternate" type="text/html" title="Bootstrap 4.4.1"/><published>2019-11-28T00:00:00+00:00</published><updated>2019-11-28T00:00:00+00:00</updated><id>https://blog.getbootstrap.com/2019/11/28/bootstrap-4-4-1</id><content type="html" xml:base="https://blog.getbootstrap.com/2019/11/28/bootstrap-4-4-1/"><p>Today we’re shipping <a href="https://github.com/twbs/bootstrap/releases/tag/v4.4.1">Bootstrap v4.4.1</a>!</p> + +<p>In <a href="/2019/11/26/bootstrap-4-4-0/">v4.4.0</a>, we added <code class="language-plaintext highlighter-rouge">add()</code> and <code class="language-plaintext highlighter-rouge">subtract()</code> functions to avoid errors when using zero values in CSS’s built in <code class="language-plaintext highlighter-rouge">calc()</code> function. While these functions work as expected with our build system, which is based on <code class="language-plaintext highlighter-rouge">node-sass</code>, some alert developers noticed that <a href="https://github.com/twbs/bootstrap/issues/29743">things broke when using another Sass compiler</a> like Dart Sass or Ruby Sass. To resolve this issue, we’ve tweaked these functions a bit to output what we would expect.</p> + +<p>Lastly, we also added a <a href="https://github.com/twbs/bootstrap/pull/29762">theming fix</a> for some custom forms in a disabled fieldset.</p> + +<p>&lt;3,<br /></p> + +<p><a href="https://github.com/mdo">@mdo</a> &amp; <a href="https://github.com/twbs">team</a></p></content><author><name>{"twitter"=>"getbootstrap"}</name></author><summary type="html">Today we’re shipping Bootstrap v4.4.1!</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.getbootstrap.com/assets/img/bootstrap-social.png"/><media:content medium="image" url="https://blog.getbootstrap.com/assets/img/bootstrap-social.png" xmlns:media="http://search.yahoo.com/mrss/"/></entry><entry><title type="html">Bootstrap 4.4.0</title><link href="https://blog.getbootstrap.com/2019/11/26/bootstrap-4-4-0/" rel="alternate" type="text/html" title="Bootstrap 4.4.0"/><published>2019-11-26T00:00:00+00:00</published><updated>2019-11-26T00:00:00+00:00</updated><id>https://blog.getbootstrap.com/2019/11/26/bootstrap-4-4-0</id><content type="html" xml:base="https://blog.getbootstrap.com/2019/11/26/bootstrap-4-4-0/"><p>Bootstrap 4 has a new update with a handful of feature changes. We’ve had quite the lengthy pull request to add responsive containers—big thanks to the developers who contribute to Bootstrap for sticking with it and helping us along the way. Nearly all new features will be carried forward into Bootstrap 5, so feel free to start using them now.</p> + +<h2 id="highlights">Highlights</h2> + +<p>Here’s what you need to know about v4.4.0. Remember that with every minor and major release of Bootstrap, we ship a new URL for our hosted docs to ensure URLs continue to work.</p> + +<ul> + <li><strong>New responsive containers!</strong> Over a year in the making, fluid up to a particular breakpoint, available for all responsive tiers.</li> + <li><strong>New responsive <code class="language-plaintext highlighter-rouge">.row-cols</code> classes</strong> for quickly specifying the number of columns across breakpoints. This one is huge for those of you who have asked for responsive card decks.</li> + <li><strong>New <code class="language-plaintext highlighter-rouge">escape-svg()</code> function</strong> for simplifying our embedded <code class="language-plaintext highlighter-rouge">background-image</code> SVGs for forms and more.</li> + <li><strong>New <code class="language-plaintext highlighter-rouge">add()</code> and <code class="language-plaintext highlighter-rouge">subtract()</code> functions</strong> for avoiding errors and zero values from CSS’s built in <code class="language-plaintext highlighter-rouge">calc</code> feature.</li> + <li><strong>New <code class="language-plaintext highlighter-rouge">make-col-auto()</code> mixin</strong> to make our <code class="language-plaintext highlighter-rouge">.col-auto</code> class available with custom HTML.</li> + <li>Fixed an issue with Microsoft Edge not picking up <code class="language-plaintext highlighter-rouge">:disabled</code> styles by moving selectors to <code class="language-plaintext highlighter-rouge">[disabled]</code>.</li> + <li><strong>Deprecated:</strong> <code class="language-plaintext highlighter-rouge">bg-variant()</code>, <code class="language-plaintext highlighter-rouge">nav-divider()</code>, and <code class="language-plaintext highlighter-rouge">form-control-focus()</code> mixins are now deprecated as they’re going away in v5.</li> + <li>Updated our spacing and alignment for modal footer elements like buttons to automatically wrap when space is constrained.</li> + <li>More flexible form control validation styles thanks to fewer chained selectors. Also updated the <code class="language-plaintext highlighter-rouge">:invalid</code> validation icon to be an alert instead of an <code class="language-plaintext highlighter-rouge">&amp;times;</code> to avoid confusion with browser functionality for clearing the form field value.</li> + <li>Fixed a couple dozen CSS and JS bugs.</li> + <li>Moved to GitHub Actions for CI/CD! Expect more updates to our CI setup over time here while Actions evolves.</li> + <li>Updated documentation to fix links and typos, improved landmarks for secondary navigation, and a new security doc for guidelines on reporting potential vulnerabilities.</li> +</ul> + +<p>We’ve shipped a lot more in this release, so be sure to check out the <a href="https://github.com/twbs/bootstrap/issues?q=project%3Atwbs%2Fbootstrap%2F18+is%3Aclosed+sort%3Aupdated-desc">v4.4.0 ship list of closed issues and merged pull requests</a> for more details.</p> + +<p><a href="https://getbootstrap.com/docs/4.4/">Head to to the v4.4.0 docs</a> to see the latest in action. The full release has been published to npm and will soon appear on the BootstrapCDN and Rubygems.</p> + +<h2 id="support-the-team">Support the team</h2> + +<p>Visit our <a href="https://opencollective.com/bootstrap">Open Collective page</a> or our <a href="https://github.com/orgs/twbs/people">team members</a>’ GitHub profiles to help support the maintainers contributing to Bootstrap.</p> + +<p>&lt;3,<br /></p> + +<p><a href="https://github.com/mdo">@mdo</a> &amp; <a href="https://github.com/twbs">team</a></p></content><author><name>{"twitter"=>"getbootstrap"}</name></author><summary type="html">Bootstrap 4 has a new update with a handful of feature changes. We’ve had quite the lengthy pull request to add responsive containers—big thanks to the developers who contribute to Bootstrap for sticking with it and helping us along the way. Nearly all new features will be carried forward into Bootstrap 5, so feel free to start using them now.</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.getbootstrap.com/assets/img/bootstrap-social.png"/><media:content medium="image" url="https://blog.getbootstrap.com/assets/img/bootstrap-social.png" xmlns:media="http://search.yahoo.com/mrss/"/></entry><entry><title type="html">Introducing Bootstrap Icons</title><link href="https://blog.getbootstrap.com/2019/11/26/bootstrap-icons/" rel="alternate" type="text/html" title="Introducing Bootstrap Icons"/><published>2019-11-26T00:00:00+00:00</published><updated>2019-11-26T00:00:00+00:00</updated><id>https://blog.getbootstrap.com/2019/11/26/bootstrap-icons</id><content type="html" xml:base="https://blog.getbootstrap.com/2019/11/26/bootstrap-icons/"><p>Say hello to <a href="https://icons.getbootstrap.com/">Bootstrap Icons</a>, our very first icon set that’s designed entirely by our team and open sourced for everyone to use, with or without Bootstrap. It’s still in alpha, but we’re incredibly excited to share it with y’all ahead of our v5 alpha.</p> + +<p><a href="https://icons.getbootstrap.com/"><img src="/assets/img/2019/11/bi-docs.png" alt="Bootstrap Icons docs" /></a></p> + +<p>For the longest time, I’ve wanted to design an icon set to better lean how to better draw with different pen tools and to better understand SVGs. In the last several months I’ve used a few different applications, done nearly five style iterations, and finally settled on a single direction. The result? Over 200 icons to start.</p> + +<p><img src="/assets/img/2019/11/bi-icons.png" alt="Bootstrap Icons full list" width="300" style="margin: 0 auto;" /></p> + +<p>I’ve designed these initial icons in Figma and exported them as SVGs. The plan is to share that Figma file publicly once it’s cleaned up and the icon set is more stable. While Bootstrap Icons are first and foremost designed to work with Bootstrap’s components, they can be used anywhere. They’re an entirely <a href="https://github.com/twbs/icons">separate project</a> and package from Bootstrap, so you can easily use them in any project, or use any other icon set alongside Bootstrap’s CSS and JavaScript.</p> + +<p>They’re also open sourced under the MIT license, so you’re free to download, use, and customize as you need. This is an alpha release for now, so bear with us as we get familiar with creating and managing hundreds of SVGs. and more icons will be added over time.</p> + +<p>Head to <a href="https://icons.getbootstrap.com">https://icons.getbootstrap.com</a> to explore and download!</p> + +<p>&lt;3,<br /></p> + +<p><a href="https://github.com/mdo">@mdo</a> &amp; <a href="https://github.com/twbs">team</a></p></content><author><name>{"twitter"=>"getbootstrap"}</name></author><summary type="html">Say hello to Bootstrap Icons, our very first icon set that’s designed entirely by our team and open sourced for everyone to use, with or without Bootstrap. It’s still in alpha, but we’re incredibly excited to share it with y’all ahead of our v5 alpha.</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.getbootstrap.com/assets/img/bootstrap-social.png"/><media:content medium="image" url="https://blog.getbootstrap.com/assets/img/bootstrap-social.png" xmlns:media="http://search.yahoo.com/mrss/"/></entry><entry><title type="html">Introducing our Long Term Support plan</title><link href="https://blog.getbootstrap.com/2019/07/24/lts-plan/" rel="alternate" type="text/html" title="Introducing our Long Term Support plan"/><published>2019-07-24T00:00:00+00:00</published><updated>2019-07-24T00:00:00+00:00</updated><id>https://blog.getbootstrap.com/2019/07/24/lts-plan</id><content type="html" xml:base="https://blog.getbootstrap.com/2019/07/24/lts-plan/"><p>Today we’re formally announcing our <a href="https://github.com/twbs/release">Long Term Support plan</a>, a documented approach aimed at strengthening the stability and frequency of releases. As part of this initiative, each major version of Bootstrap will receive at least six months of support after it is retired, followed by six months of critical bug fixes and security updates.</p> + +<p>Starting today, Bootstrap 3 will move to end of life, and will no longer receive critical security updates.</p> + +<p>Bootstrap 4 will move to Long Term Support after we release <code class="language-plaintext highlighter-rouge">v4.4</code> and will no longer receive new features from then on. It will continue to receive bug fixes, security updates, and documentation updates.</p> + +<p>Bootstrap 5 is under active development. You can follow our progress <a href="https://github.com/twbs/bootstrap">on GitHub</a>.</p> + +<p>A special thanks to <a href="https://github.com/XhmikosR">@XhmikosR</a> for his tireless work in pushing us forward.</p> + +<p>&lt;3,<br /></p> + +<p><a href="https://github.com/mdo">@mdo</a> &amp; <a href="https://github.com/twbs">team</a></p></content><author><name>{"twitter"=>"getbootstrap"}</name></author><summary type="html">Today we’re formally announcing our Long Term Support plan, a documented approach aimed at strengthening the stability and frequency of releases. As part of this initiative, each major version of Bootstrap will receive at least six months of support after it is retired, followed by six months of critical bug fixes and security updates.</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.getbootstrap.com/assets/img/bootstrap-social.png"/><media:content medium="image" url="https://blog.getbootstrap.com/assets/img/bootstrap-social.png" xmlns:media="http://search.yahoo.com/mrss/"/></entry><entry><title type="html">Bootstrap 3.4.1 and 4.3.1</title><link href="https://blog.getbootstrap.com/2019/02/13/bootstrap-4-3-1-and-3-4-1/" rel="alternate" type="text/html" title="Bootstrap 3.4.1 and 4.3.1"/><published>2019-02-13T00:00:00+00:00</published><updated>2019-02-13T00:00:00+00:00</updated><id>https://blog.getbootstrap.com/2019/02/13/bootstrap-4-3-1-and-3-4-1</id><content type="html" xml:base="https://blog.getbootstrap.com/2019/02/13/bootstrap-4-3-1-and-3-4-1/"><p>Today we’re shipping <a href="https://github.com/twbs/bootstrap/releases/tag/v4.3.1">Bootstrap v4.3.1</a> and <a href="https://github.com/twbs/bootstrap/releases/tag/v3.4.1">v3.4.1</a> to patch an XSS vulnerability, CVE-2019-8331. Also included in v4.3.1 is a small fix to some RFS (responsive font sizes) mixins that were added in v4.3.0.</p> + +<p>Earlier this week a developer reported an XSS issue similar to the <code class="language-plaintext highlighter-rouge">data-target</code> vulnerability that was fixed in v4.1.2 and v3.4.0: the <code class="language-plaintext highlighter-rouge">data-template</code> attribute for our tooltip and popover plugins lacked proper XSS sanitization of the HTML that can be passed into the attribute’s value.</p> + +<p>To resolve the issue, we’ve implemented a new JavaScript sanitizer to only allow whitelisted HTML elements in data attribute. You may modify our sanitization implementation to customize the HTML element whitelist, totally disable the sanitization, or pass your own sanitize function (useful if you use your own library). However, for added protection, there is no way to modify the sanitization via data attributes—you must modify these plugin options via the JavaScript API.</p> + +<p>Those who have modified the default templates, please <a href="https://getbootstrap.com/docs/4.3/getting-started/javascript/#sanitizer">read the new v4.3 sanitizer docs</a> or the <a href="https://getbootstrap.com/docs/3.4/javascript/#js-sanitizer">new v3.4 sanitizer docs</a>.</p> + +<p>In light of this vulnerability, we’re also auditing our security reporting workflows to ensure they’re up to date. This will include steps like adding a <code class="language-plaintext highlighter-rouge">SECURITY.md</code> file to our repository and ensuring our private channels and processes are up to date and documented with the team.</p> + +<p>Thank you to <a href="https://www.drupal.org/u/poiu">poiu</a> for reporting the vulnerability to the <a href="https://www.drupal.org/project/bootstrap">Bootstrap Drupal project</a> and <a href="https://www.drupal.org/u/markcarver">Mark Carver</a> from the <a href="https://www.drupal.org/project/bootstrap">Bootstrap Drupal project</a> for responsibly disclosing the issue to us. Also a massive thank you to <a href="https://github.com/johann-s">@Johann-S</a>, <a href="https://github.com/xhmikosr">@Xhmikosr</a>, and <a href="https://github.com/bardiharborow">@bardiharborow</a> on our team for the fast turnaround on today’s releases.</p> + +<p>&lt;3,<br /> +<a href="https://twitter.com/mdo">@mdo</a> &amp; <a href="https://github.com/twbs">team</a></p></content><author><name>{"twitter"=>"getbootstrap"}</name></author><summary type="html">Today we’re shipping Bootstrap v4.3.1 and v3.4.1 to patch an XSS vulnerability, CVE-2019-8331. Also included in v4.3.1 is a small fix to some RFS (responsive font sizes) mixins that were added in v4.3.0.</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.getbootstrap.com/assets/img/bootstrap-social.png"/><media:content medium="image" url="https://blog.getbootstrap.com/assets/img/bootstrap-social.png" xmlns:media="http://search.yahoo.com/mrss/"/></entry><entry><title type="html">Bootstrap 4.3.0</title><link href="https://blog.getbootstrap.com/2019/02/11/bootstrap-4-3-0/" rel="alternate" type="text/html" title="Bootstrap 4.3.0"/><published>2019-02-11T00:00:00+00:00</published><updated>2019-02-11T00:00:00+00:00</updated><id>https://blog.getbootstrap.com/2019/02/11/bootstrap-4-3-0</id><content type="html" xml:base="https://blog.getbootstrap.com/2019/02/11/bootstrap-4-3-0/"><p>Bootstrap v4.3 has landed with over 120 combined closed issues and merged pull requests. This release brings improvements to our utilities, some prep work for moving on to v5’s development, and the standard bug fixes and documentation updates.</p> + +<p>During our last release, we shared a small preview of where we’re taking the project next. That’s getting clearer in the coming weeks as our attention turns towards embracing Hugo for ultra fast docs development, removing jQuery in favor of regular JavaScript, and addressing our growing code base.</p> + +<p>Keep reading for v4.3 highlights, and see you soon with more details on v5!</p> + +<h2 id="highlights">Highlights</h2> + +<p>We’ve added some new utilities and deprecated some unused code. Here are the key changes in v4.3, broken down by new, improved, fixed, and deprecated.</p> + +<ul> + <li><strong>New:</strong> Added <code class="language-plaintext highlighter-rouge">.stretched-link</code> utility to make any anchor the size of it’s nearest <code class="language-plaintext highlighter-rouge">position: relative</code> parent, perfect for entirely clickable cards!</li> + <li><strong>New:</strong> Added <code class="language-plaintext highlighter-rouge">.text-break</code> utility for applying <code class="language-plaintext highlighter-rouge">word-break: break-word</code></li> + <li><strong>New:</strong> Added <code class="language-plaintext highlighter-rouge">.rounded-sm</code> and <code class="language-plaintext highlighter-rouge">.rounded-lg</code> for small and large <code class="language-plaintext highlighter-rouge">border-radius</code>.</li> + <li><strong>New:</strong> Added <code class="language-plaintext highlighter-rouge">.modal-dialog-scrollable</code> modifier class for scrolling content <em>within</em> a modal.</li> + <li><strong>New:</strong> Added responsive <code class="language-plaintext highlighter-rouge">.list-group-horizontal</code> modifier classes for displaying list groups as a horizontal row.</li> + <li><strong>Improved:</strong> Reduced our compiled CSS by using <code class="language-plaintext highlighter-rouge">null</code> for variables that by default inherit their values from other elements (e.g., <code class="language-plaintext highlighter-rouge">$headings-color</code> was <code class="language-plaintext highlighter-rouge">inherit</code> and is now <code class="language-plaintext highlighter-rouge">null</code> until you modifier it in your custom CSS).</li> + <li><strong>Improved:</strong> Badge focus styles now match their <code class="language-plaintext highlighter-rouge">background-color</code> like our buttons.</li> + <li><strong>Fixed:</strong> Silenced bad selectors in our JS plugins for the <code class="language-plaintext highlighter-rouge">href</code> HTML attribute to avoid JavaScript errors. Please try to use <a href="https://www.w3.org/TR/CSS21/syndata.html#value-def-identifier">valid selectors</a> or the <code class="language-plaintext highlighter-rouge">data-target</code> HTML attribute/<code class="language-plaintext highlighter-rouge">target</code> option where available.</li> + <li><strong>Fixed:</strong> Reverted v4.2.1’s change to the breakpoint and grid container Sass maps that blocked folks from upgrading when modifying those default variables.</li> + <li><strong>Fixed:</strong> Restored <code class="language-plaintext highlighter-rouge">white-space: nowrap</code> to <code class="language-plaintext highlighter-rouge">.dropdown-toggle</code> (before v4.2.1 it was on all <code class="language-plaintext highlighter-rouge">.btn</code>s) so carets don’t wrap to new lines.</li> + <li><strong>Deprecated:</strong> <code class="language-plaintext highlighter-rouge">img-retina</code>, <code class="language-plaintext highlighter-rouge">invisible</code>, <code class="language-plaintext highlighter-rouge">float</code>, and <code class="language-plaintext highlighter-rouge">size</code> mixins are now deprecated and will be removed in v5.</li> +</ul> + +<p>Checkout the full <a href="https://github.com/twbs/bootstrap/issues/27893">v4.3.0 ship list</a> and <a href="https://github.com/twbs/bootstrap/projects/16">GitHub project</a> for the full details.</p> + +<p><a href="https://getbootstrap.com/docs/4.3/">Head to to the v4.3.x docs</a> to see the latest in action. The full release has been published to npm and will soon appear on the Bootstrap CDN and Rubygems.</p> + +<h2 id="introducing-responsive-font-sizes">Introducing responsive font sizes</h2> + +<p><img src="/assets/img/2019/02/rfs.png" alt="Responsive font-sizes" /></p> + +<p>Our biggest new addition to Bootstrap in v4.3 is responsive font sizes, a <a href="https://github.com/twbs/rfs">new project in the Bootstrap GitHub org</a> to automate calculate an appropriate <code class="language-plaintext highlighter-rouge">font-size</code> based on the dimensions of a visitor’s device or browser viewport. Here’s how it works:</p> + +<ul> + <li> + <p>All <code class="language-plaintext highlighter-rouge">font-size</code> properties have been switched to the <code class="language-plaintext highlighter-rouge">@include font-size()</code> mixin. Our Stylelint configuration now prevents the usage of <code class="language-plaintext highlighter-rouge">font-size</code> property.</p> + </li> + <li> + <p>Disabled by default, you can opt into this new behavior by toggling the <code class="language-plaintext highlighter-rouge">$enable-responsive-font-sizes</code> boolean variable.</p> + </li> + <li> + <p><code class="language-plaintext highlighter-rouge">font-size</code>s are entirely configurable via Sass. Be sure to read the docs for how to modify the scales, variables, and more.</p> + </li> +</ul> + +<p>While responsive font-sizes are disabled by default, we’ve enabled them in the custom CSS that powers our docs starting with v4.3. Please share feedback with us via GitHub issues or on Twitter. We’ve added some light guidance to <a href="https://getbootstrap.com/docs/4.3/content/typography/#responsive-font-sizes">our Typography docs</a> to explain the feature. You can also learn more by <a href="https://github.com/twbs/rfs">reading the rfs project documentation</a>.</p> + +<h2 id="open-collective">Open Collective</h2> + +<p>Last December we launched our <a href="https://opencollective.com/bootstrap">Open Collective page</a> with our <a href="/2018/12/13/bootstrap-3-4-0/">v3.4 release</a> to help support the maintainers contributing to Bootstrap. The team has been very excited about this as a way to be transparent about maintainer costs (both time and money), as well as recognition of efforts.</p> + +<h2 id="branches-hugo-and-jquery">Branches, Hugo, and jQuery</h2> + +<p>Right after shipping v4.3, we’ll be tackling a few key changes on our road to active v5 development. These are larger changes to how we maintain and develop Bootstrap and are considered foundational for v5.</p> + +<ul> + <li> + <p><strong>Improving our branches for development.</strong> <code class="language-plaintext highlighter-rouge">master</code> will become our new <code class="language-plaintext highlighter-rouge">v3-dev</code> branch. <code class="language-plaintext highlighter-rouge">v4-dev</code> will stay as-is, but we’ll cut a new <code class="language-plaintext highlighter-rouge">master</code> branch from there to develop v5.</p> + </li> + <li> + <p><strong>We’re moving to Hugo!</strong> Jekyll has been great, but it’s starting to slow us down in local development. We’ll be making changes to our dependencies to support this move, and there’s already a pull request in progress and near completion for the change. <a href="https://github.com/twbs/bootstrap/pull/28014">Follow along</a> to see what’s changing.</p> + </li> + <li> + <p><strong>We’re dropping jQuery for regular JavaScript.</strong> The cat is out of the bag—we’re dropping our largest client-side dependency for regular JavaScript. Similar to the Hugo move, we’ve been working on this for a long time and have a <a href="https://github.com/twbs/bootstrap/pull/23586">pull request in progress</a> and near completion.</p> + </li> +</ul> + +<p>We’ll have even more to share soon around v5’s plans after we tackle these bigger items. In the meantime, keep the feedback coming on GitHub and Twitter!</p> + +<p>&lt;3,<br /> +<a href="https://twitter.com/mdo">@mdo</a> &amp; <a href="https://github.com/twbs">team</a></p></content><author><name>{"twitter"=>"getbootstrap"}</name></author><summary type="html">Bootstrap v4.3 has landed with over 120 combined closed issues and merged pull requests. This release brings improvements to our utilities, some prep work for moving on to v5’s development, and the standard bug fixes and documentation updates.</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.getbootstrap.com/assets/img/bootstrap-social.png"/><media:content medium="image" url="https://blog.getbootstrap.com/assets/img/bootstrap-social.png" xmlns:media="http://search.yahoo.com/mrss/"/></entry><entry><title type="html">Bootstrap 4.2.1</title><link href="https://blog.getbootstrap.com/2018/12/21/bootstrap-4-2-1/" rel="alternate" type="text/html" title="Bootstrap 4.2.1"/><published>2018-12-21T00:00:00+00:00</published><updated>2018-12-21T00:00:00+00:00</updated><id>https://blog.getbootstrap.com/2018/12/21/bootstrap-4-2-1</id><content type="html" xml:base="https://blog.getbootstrap.com/2018/12/21/bootstrap-4-2-1/"><p>Look out world, we’re shipping Bootstrap v4.2.1 with a slew of new features, bug fixes, and docs updates. On the new features side, we have spinners, toasts, switches, and (finally!) touch support in the carousel. That’s just the tip of the iceberg though.</p> + +<p><strong>Heads up!</strong> v4.2.0 was incorrectly published to npm, so we’ve had to immediately turnaround a v4.2.1 release. <code class="language-plaintext highlighter-rouge">npm i bootstrap@latest</code> should now return <code class="language-plaintext highlighter-rouge">4.2.1</code>. Apologies for the inconvenience!</p> + +<p>We’ve crammed months of work into v4.2.1 with over 400 commits since our last v4.1.3 release. As mentioned in our v3.4.0 release last week, we’re working to decouple our releases from my direct involvement to improve the shipping cadence. Expect more improvements there in 2019.</p> + +<p>Keep reading for highlights and some insight into how we’re getting to v4.3 quickly, and then into v5 (woo!).</p> + +<h2 id="whats-new">What’s new</h2> + +<p>Here are the highlights of what’s new and updated in v4.2.1.</p> + +<p><img src="/assets/img/2018/12/toasts.png" alt="Bootstrap toasts" /></p> + +<ul> + <li><strong>New:</strong> Added a new <a href="https://getbootstrap.com/docs/4.2/components/spinners/">spinner loading component</a>.</li> + <li><strong>New:</strong> Added new <a href="https://getbootstrap.com/docs/4.2/components/toasts/">toast component</a> for displaying notifications.</li> + <li><strong>New:</strong> Added a new <a href="https://getbootstrap.com/docs/4.2/components/forms/#switches">iOS style switch</a> (a modifier class to our custom checkboxes).</li> + <li><strong>New:</strong> Added touch support in our carousel component.</li> + <li><strong>New:</strong> Added <code class="language-plaintext highlighter-rouge">.font-weight-lighter</code> and <code class="language-plaintext highlighter-rouge">.font-weight-bolder</code> utilities.</li> + <li><strong>New:</strong> Added <code class="language-plaintext highlighter-rouge">.text-decoration-none</code> utility class.</li> + <li><strong>New:</strong> Added <code class="language-plaintext highlighter-rouge">.modal-xl</code> modifier class for our modals.</li> + <li><strong>New:</strong> Added new negative margin utility classes (e.g., <code class="language-plaintext highlighter-rouge">.mb-n3</code>). These rad new classes not only allow you more control over your general spacing needs, but also allow you to create responsive grid gutters at each breakpoint.</li> + <li><strong>New:</strong> Validated form fields now have feedback icons on <code class="language-plaintext highlighter-rouge">:invalid</code> and <code class="language-plaintext highlighter-rouge">:valid</code> fields. Disable them with the <code class="language-plaintext highlighter-rouge">$enable-validation-icons</code> boolean Sass variable (defaults to <code class="language-plaintext highlighter-rouge">true</code>).</li> + <li><strong>New:</strong> Added a new <a href="https://getbootstrap.com/docs/versions/">versions page</a> to our docs.</li> + <li><strong>New:</strong> Tooltips/Popovers work with Shadow DOM.</li> + <li><strong>Updated:</strong> Redesigned the custom checkboxes and radios for more obvious states.</li> + <li><strong>Updated:</strong> <code class="language-plaintext highlighter-rouge">bootstrap-grid.css</code> now includes our <code class="language-plaintext highlighter-rouge">margin</code> and <code class="language-plaintext highlighter-rouge">padding</code> utilities for full control of our grid system.</li> + <li><strong>Updated:</strong> Changed auto columns (e.g., <code class="language-plaintext highlighter-rouge">.col-auto</code>) from <code class="language-plaintext highlighter-rouge">max-width: none</code> to <code class="language-plaintext highlighter-rouge">max-width: 100%</code> to prevent content from causing a column to overflow the parent.</li> + <li><strong>Updated:</strong> Improved rendering of custom selects, ranges, file input, and more.</li> +</ul> + +<p>Checkout the full <a href="https://github.com/twbs/bootstrap/issues/26952">v4.2.0 ship list</a> and <a href="https://github.com/twbs/bootstrap/projects/6">GitHub project</a> for the full details. Up next is <a href="https://github.com/twbs/bootstrap/projects/16">v4.3</a> with some bugfixes, a few new modifier classes and variables, and some new utilities.</p> + +<p><a href="https://getbootstrap.com/docs/4.2/">Head to the v4.2 docs</a> to see the latest in action. The full release has been published to npm and will soon appear on the Bootstrap CDN and Rubygems.</p> + +<h2 id="whats-next">What’s next</h2> + +<p>We have <a href="https://github.com/twbs/bootstrap/projects/16">v4.3</a> already planned, so that’s our immediate focus. However, while we’re developing that in the <code class="language-plaintext highlighter-rouge">v4-dev</code> branch, we’ll be getting our plans in order for a v5 release.</p> + +<p>Bootstrap 5 will not feature drastic changes to the codebase. While I tweeted about the earnestness to move to PostCSS years ago, we’ll still be on Sass for v5. Instead, we’ll focus our efforts on removing cruft, improving existing components, and dropping old browsers and our jQuery dependency. There are also some updates to our v4.x components we cannot make without causing breaking changes, so v5 feels like it’s coming at the right time for us.</p> + +<p>Stay tuned for a preview of the plans for v5 in the new year. We’ll share via an issue, ask for feedback, and then settle in to development mode.</p> + +<p>Happy holidays, and happy new year to everyone! Thanks for continuing to make Bootstrap an amazing project and community.</p> + +<p>&lt;3,<br /> +<a href="https://twitter.com/mdo">@mdo</a> &amp; <a href="https://github.com/twbs">team</a></p></content><author><name>{"twitter"=>"getbootstrap"}</name></author><summary type="html">Look out world, we’re shipping Bootstrap v4.2.1 with a slew of new features, bug fixes, and docs updates. On the new features side, we have spinners, toasts, switches, and (finally!) touch support in the carousel. That’s just the tip of the iceberg though.</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.getbootstrap.com/assets/img/bootstrap-social.png"/><media:content medium="image" url="https://blog.getbootstrap.com/assets/img/bootstrap-social.png" xmlns:media="http://search.yahoo.com/mrss/"/></entry><entry><title type="html">Bootstrap 3.4.0</title><link href="https://blog.getbootstrap.com/2018/12/13/bootstrap-3-4-0/" rel="alternate" type="text/html" title="Bootstrap 3.4.0"/><published>2018-12-13T00:00:00+00:00</published><updated>2018-12-13T00:00:00+00:00</updated><id>https://blog.getbootstrap.com/2018/12/13/bootstrap-3-4-0</id><content type="html" xml:base="https://blog.getbootstrap.com/2018/12/13/bootstrap-3-4-0/"><p>That’s not a typo—today we’re shipping Bootstrap 3.4.0, a long overdue update to address some quality of life issues, XSS fixes, and build tooling updates to make it easier for us, and you, to develop.</p> + +<p>While we’d planned for ages to do a fresh v3.x update, we lost steam as energy was focused on all the work in v4. Early this year, <a href="https://github.com/twbs/bootstrap/issues/25679">one issue in particular</a> gained a ton of momentum from the community and the core team decided to do a huge push to pull together a solid release. I regret the time it took to pull this release together, especially given the security fixes, but with the improvements under the hood, v3 has never been easier to develop and maintain. Thanks for your continued support along the way!</p> + +<p>Keep reading for what’s changed and a look ahead at what’s coming in v4.2.0.</p> + +<h2 id="whats-new">What’s new</h2> + +<p>While we haven’t publicly worked on v3.x in years, we’ve heard from all of you during that time that we needed to do a new release to address</p> + +<ul> + <li><strong>New:</strong> Added a <code class="language-plaintext highlighter-rouge">.row-no-gutters</code> class.</li> + <li><strong>New:</strong> Added docs searching via Algolia.</li> + <li><strong>Fixed:</strong> Resolved an XSS issue in Alert, Carousel, Collapse, Dropdown, Modal, and Tab components. See <a href="https://snyk.io/vuln/npm:bootstrap:20160627">https://snyk.io/vuln/npm:bootstrap:20160627</a> for details.</li> + <li><strong>Fixed:</strong> Added padding to <code class="language-plaintext highlighter-rouge">.navbar-fixed-*</code> on modal open</li> + <li><strong>Fixed:</strong> Removed the double border on <code class="language-plaintext highlighter-rouge">&lt;abbr&gt;</code> elements.</li> + <li>Removed Gist creation in web-based Customizer since anonymous gists were disabled long ago by GitHub.</li> + <li>Removed drag and drop support from Customizer since it didn’t work anymore.</li> +</ul> + +<p>Our documentation and tooling saw massive updates as well to make it easier to work on v3.x, for ourselves and for you.</p> + +<ul> + <li>Added a dropdown to the docs nav for newer and previous versions.</li> + <li>Update the docs to use a new <code class="language-plaintext highlighter-rouge">baseurl</code>, <code class="language-plaintext highlighter-rouge">/docs/3.4/</code>, to version the v3.x documentation like we do with v4.</li> + <li>Reorganized the v3 docs CSS to use Less.</li> + <li>Switched to BrowserStack for tests.</li> + <li>Updated links to always use https and fix broken URLs.</li> + <li>Replaced ZeroClipboard with clipboard.js</li> +</ul> + +<p><a href="https://getbootstrap.com/docs/3.4/">Head to the Bootstrap 3.4 docs</a> to see the latest in action. <a href="https://github.com/twbs/bootstrap/pull/27288">Check out the v3.4.0 pull request</a> for even more context on what’s changed.</p> + +<h2 id="upgrading">Upgrading</h2> + +<p>Upgrade your Bootstrap 3 projects to v3.4.0 with <code class="language-plaintext highlighter-rouge">npm i bootstrap@previous</code> or <code class="language-plaintext highlighter-rouge">npm i bootstrap@3.4.0</code>. This release won’t be available via Bower to start given the package manager was deprecated and has largely been unused by us in v4 for well over a year. Stay tuned for CDN and Rubygem updates.</p> + +<h2 id="open-collective">Open Collective</h2> + +<p>Also new with our v3.4 is the creation of an <a href="https://opencollective.com/bootstrap">Open Collective page</a> to help support the maintainers contributing to Bootstrap. The team has been very excited about this as a way to be transparent about maintainer costs (both time and money), as well as recognition of efforts.</p> + +<h2 id="v42-and-beyond">v4.2 and beyond</h2> + +<p>We’ve been working on a <a href="https://github.com/twbs/bootstrap/projects/6?fullscreen=true">huge v4.2 update</a> for several months now. Our attention has largely been on advancing the project and simplifying it’s dependencies, namely by removing our jQuery dependency. That work has sparked a keen interest in a moderately scoped v5 release, so we’ve been taking our sweet time with v4.2 to sneak in as many new features as we can.</p> + +<p>After we ship v4.2, we’ll plan for point releases to address any bugs and improvements as y’all start to use the new version. From there, we’ll start to share more plans on v5 to remove jQuery, drop support for older browsers, and clear up some cruft. This won’t be a sweeping rewrite, but rather an iterative improvement on v4. Stay tuned!</p> + +<p>&lt;3,<br /> +<a href="https://twitter.com/mdo">@mdo</a> &amp; <a href="https://github.com/twbs">team</a></p></content><author><name>{"twitter"=>"getbootstrap"}</name></author><summary type="html">That’s not a typo—today we’re shipping Bootstrap 3.4.0, a long overdue update to address some quality of life issues, XSS fixes, and build tooling updates to make it easier for us, and you, to develop.</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.getbootstrap.com/assets/img/bootstrap-social.png"/><media:content medium="image" url="https://blog.getbootstrap.com/assets/img/bootstrap-social.png" xmlns:media="http://search.yahoo.com/mrss/"/></entry><entry><title type="html">Bootstrap 4.1.3</title><link href="https://blog.getbootstrap.com/2018/07/24/bootstrap-4-1-3/" rel="alternate" type="text/html" title="Bootstrap 4.1.3"/><published>2018-07-24T00:00:00+00:00</published><updated>2018-07-24T00:00:00+00:00</updated><id>https://blog.getbootstrap.com/2018/07/24/bootstrap-4-1-3</id><content type="html" xml:base="https://blog.getbootstrap.com/2018/07/24/bootstrap-4-1-3/"><p>Hot on the heels of v4.1.2, we’re shipping another patch release to address an issue with our browserslist config, fix some CSS bugs, make JavaScript plugins UMD ready, and improve form control rendering. Up next will be v4.2, our second minor release where we add some new features.</p> + +<p>But first, here are the highlights for v4.1.3. Pay attention to the change to <code class="language-plaintext highlighter-rouge">.form-control</code>s which adds a new fixed <code class="language-plaintext highlighter-rouge">height</code>.</p> + +<ul> + <li><strong>Fixed:</strong> Moved the browserslist config from our <code class="language-plaintext highlighter-rouge">package.json</code> to a separate file to avoid unintended inherited browser settings across npm projects.</li> + <li><strong>Fixed:</strong> Removed the <code class="language-plaintext highlighter-rouge">:not(:root)</code> selector from our <code class="language-plaintext highlighter-rouge">svg</code> Reboot styles, resolving an issue that caused all inline SVGs ignore <code class="language-plaintext highlighter-rouge">vertical-align</code> styles via single class due to higher specificity.</li> + <li><strong>Fixed:</strong> Buttons in custom file inputs are once again clickable when focused.</li> + <li><strong>Improved:</strong> Bootstrap’s plugins can now be imported separately in any contexts because they are now UMD ready.</li> + <li><strong>Improved:</strong> <code class="language-plaintext highlighter-rouge">.form-control</code>s now have a fixed <code class="language-plaintext highlighter-rouge">height</code> to compensate for differences in computed height across different <code class="language-plaintext highlighter-rouge">type</code>s. This also fixes some IE alignment issues.</li> + <li><strong>Improved:</strong> Added <code class="language-plaintext highlighter-rouge">Noto Color Emoji</code> to our system font stack for better rendering in Linux OSes.</li> +</ul> + +<p>Checkout the full <a href="https://github.com/twbs/bootstrap/issues/26867">v4.1.3 ship list</a> and <a href="https://github.com/twbs/bootstrap/projects/15">GitHub project</a> for the full details. Up next is <a href="https://github.com/twbs/bootstrap/projects/6">v4.2</a>, so stay tuned for some awesome new features like toasts, dismissible badges, negative margins (responsive grid gutters!), spinners, and more!</p> + +<p><a href="https://getbootstrap.com/docs/4.1/">Head to the v4.1.x docs</a> to see the latest in action. The full release has been published to npm and will soon appear on the Bootstrap CDN and Rubygems.</p> + +<p>&lt;3,<br /> +<a href="https://twitter.com/mdo">@mdo</a> &amp; <a href="https://github.com/twbs">team</a></p></content><author><name>{"twitter"=>"getbootstrap"}</name></author><summary type="html">Hot on the heels of v4.1.2, we’re shipping another patch release to address an issue with our browserslist config, fix some CSS bugs, make JavaScript plugins UMD ready, and improve form control rendering. Up next will be v4.2, our second minor release where we add some new features.</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.getbootstrap.com/assets/img/bootstrap-social.png"/><media:content medium="image" url="https://blog.getbootstrap.com/assets/img/bootstrap-social.png" xmlns:media="http://search.yahoo.com/mrss/"/></entry></feed> +\ No newline at end of file diff --git a/src/test/resources/suumo.xml b/src/test/resources/suumo.xml @@ -0,0 +1,225 @@ +<?xml version="1.0" encoding="UTF-8"?> +<rss version="2.0"> + <channel> + <title>SUUMO(スーモ)</title> + <link>https://suumo.jp/jj/bukken/ichiran/JJ012FC001/?ar=030&bs=021&nf=021001</link> + <pubDate>Thu, 23 Jan 2020 21:54:47 +0900</pubDate> + <language>ja</language> + <description>【検索条件】|今週の新着物件</description> + <image> + <url>https://suumo.jp/jj</url> + <title>SUUMO(スーモ)</title> + <link>https://suumo.jp/jj/bukken/ichiran/JJ012FC001/?ar=030&bs=021&nf=021001</link> + </image> +<item> + <title> + 物件名:自然の中に佇む、南房総の家</title> + <link>https://suumo.jp/jj/bukken/shosai/JJ010FJ100/?ar=030&bs=021&nc=93385930&ta=12&sc=12234</link> + <description> + 千葉県南房総市山田990万円JR内房線岩井623m&sup2;(登記)60.45m&sup2;(登記)1LDK1998年10月</description> + </item> + <item> + <title> + 物件名:大字高萩(武蔵高萩駅) 1180万円</title> + <link>https://suumo.jp/jj/bukken/shosai/JJ010FJ100/?ar=030&bs=021&nc=93373816&ta=11&sc=11242</link> + <description> + 埼玉県日高市大字高萩1180万円JR川越線武蔵高萩徒歩9分110.18m&sup2;(登記)87.58m&sup2;(登記)4LDK1980年2月</description> + </item> + <item> + <title> + 物件名:前橋市上細井町 中古戸建</title> + <link>https://suumo.jp/jj/bukken/shosai/JJ010FJ100/?ar=030&bs=021&nc=93403901&ta=10&sc=10201</link> + <description> + 群馬県前橋市上細井町1750万円上毛電鉄三俣徒歩38分202.5m&sup2;110.13m&sup2;4LDK2010年9月</description> + </item> + <item> + <title> + 物件名:宮元町 1790万円</title> + <link>https://suumo.jp/jj/bukken/shosai/JJ010FJ100/?ar=030&bs=021&nc=93368238&ta=11&sc=11201</link> + <description> + 埼玉県川越市宮元町1790万円西武新宿線本川越バス6分136.79m&sup2;(実測)123.99m&sup2;(実測)4LDKK+2S(納戸)1997年12月</description> + </item> + <item> + <title> + 物件名:【 KEIAI 】 伊勢崎市市場町 リフォーム中古戸建</title> + <link>https://suumo.jp/jj/bukken/shosai/JJ010FJ100/?ar=030&bs=021&nc=93390762&ta=10&sc=10204</link> + <description> + 群馬県伊勢崎市市場町1-121-91880万円JR両毛線国定徒歩46分232.16m&sup2;118.23m&sup2;4LDK1997年10月</description> + </item> + <item> + <title> + 物件名:八千代市萱田町戸建</title> + <link>https://suumo.jp/jj/bukken/shosai/JJ010FJ100/?ar=030&bs=021&nc=93404196&ta=12&sc=12221</link> + <description> + 千葉県八千代市萱田町1890万円東葉高速鉄道八千代中央徒歩9分100.39m&sup2;(30.36坪)(登記)98.53m&sup2;(29.80坪)(登記)3LDK2003年10月</description> + </item> + <item> + <title> + 物件名:前橋市荒牧町 リフォーム中古戸建</title> + <link>https://suumo.jp/jj/bukken/shosai/JJ010FJ100/?ar=030&bs=021&nc=93404986&ta=10&sc=10201</link> + <description> + 群馬県前橋市荒牧町21998万円JR上越線群馬総社徒歩47分269.77m&sup2;89.43m&sup2;3LDK1996年1月</description> + </item> + <item> + <title> + 物件名:小平市 学園西町</title> + <link>https://suumo.jp/jj/bukken/shosai/JJ010FJ100/?ar=030&bs=021&nc=93377334&ta=13&sc=13211</link> + <description> + 東京都小平市学園西町32000万円西武多摩湖線一橋学園徒歩10分67.04m&sup2;(登記)83.36m&sup2;(登記)、うち地下車庫12.39m&sup2;4DK1992年11月</description> + </item> + <item> + <title> + 物件名:松伏町ゆめみ野東 セキスイハイム</title> + <link>https://suumo.jp/jj/bukken/shosai/JJ010FJ100/?ar=030&bs=021&nc=93360355&ta=11&sc=11460</link> + <description> + 埼玉県北葛飾郡松伏町ゆめみ野東3-9-42080万円東武伊勢崎線北越谷バス18分163.4m&sup2;(49.42坪)(登記)112.46m&sup2;(34.01坪)(登記)4LDK+S(納戸)1995年12月</description> + </item> + <item> + <title> + 物件名:前橋市上泉町 リフォーム中古戸建</title> + <link>https://suumo.jp/jj/bukken/shosai/JJ010FJ100/?ar=030&bs=021&nc=93404261&ta=10&sc=10201</link> + <description> + 群馬県前橋市上泉町2098万円上毛電鉄上泉徒歩4分243.91m&sup2;190.36m&sup2;4LDK+S(納戸)1999年12月</description> + </item> + <item> + <title> + 物件名:大字南中丸(大和田駅) 2180万円</title> + <link>https://suumo.jp/jj/bukken/shosai/JJ010FJ100/?ar=030&bs=021&nc=93359476&ta=11&sc=11104</link> + <description> + 埼玉県さいたま市見沼区大字南中丸2180万円東武野田線大和田徒歩14分100.05m&sup2;(30.26坪)92.74m&sup2;(28.05坪)4DK2000年3月</description> + </item> + <item> + <title> + 物件名:真岡市長島 中古住宅</title> + <link>https://suumo.jp/jj/bukken/shosai/JJ010FJ100/?ar=030&bs=021&nc=93367932&ta=09&sc=09209</link> + <description> + 栃木県真岡市長島2180万円真岡鉄道久下田499.9m&sup2;(151.21坪)(登記)157.5m&sup2;(47.64坪)(登記)5LDK2005年5月</description> + </item> + <item> + <title> + 物件名:馬場3 2209万円</title> + <link>https://suumo.jp/jj/bukken/shosai/JJ010FJ100/?ar=030&bs=021&nc=93391099&ta=11&sc=11230</link> + <description> + 埼玉県新座市馬場32209万円東武東上線朝霞台バス14分107.26m&sup2;(登記)85.1m&sup2;(登記)4LDK1990年3月</description> + </item> + <item> + <title> + 物件名:大字矢颪(飯能駅) 2222万円</title> + <link>https://suumo.jp/jj/bukken/shosai/JJ010FJ100/?ar=030&bs=021&nc=93377577&ta=11&sc=11209</link> + <description> + 埼玉県飯能市大字矢颪2222万円西武池袋線飯能徒歩10分121.4m&sup2;(登記)92.74m&sup2;(登記)4LDK2004年5月</description> + </item> + <item> + <title> + 物件名:西新井1丁目 中古戸建</title> + <link>https://suumo.jp/jj/bukken/shosai/JJ010FJ100/?ar=030&bs=021&nc=93376677&ta=13&sc=13121</link> + <description> + 東京都足立区西新井12280万円東武大師線大師前徒歩7分37.19m&sup2;(11.24坪)(登記)65.07m&sup2;(19.68坪)(実測)2LDK2006年8月</description> + </item> + <item> + <title> + 物件名:西新井1丁目 中古戸建 2280万円</title> + <link>https://suumo.jp/jj/bukken/shosai/JJ010FJ100/?ar=030&bs=021&nc=93395776&ta=13&sc=13121</link> + <description> + 東京都足立区西新井12280万円東武大師線大師前徒歩7分37.19m&sup2;65.07m&sup2;2LDK2006年8月</description> + </item> + <item> + <title> + 物件名:足立区保木間二丁目</title> + <link>https://suumo.jp/jj/bukken/shosai/JJ010FJ100/?ar=030&bs=021&nc=93368748&ta=13&sc=13121</link> + <description> + 東京都足立区保木間22380万円東武伊勢崎線竹ノ塚徒歩21分63.94m&sup2;(19.34坪)(登記)74.36m&sup2;(22.49坪)(登記)2LDK2001年7月</description> + </item> + <item> + <title> + 物件名:プラウドシーズン船橋小室</title> + <link>https://suumo.jp/jj/bukken/shosai/JJ010FJ100/?ar=030&bs=021&nc=93378384&ta=12&sc=12204</link> + <description> + 千葉県船橋市小室町2400万円北総線小室徒歩4分136.2m&sup2;(41.20坪)(登記)101.02m&sup2;(30.55坪)4LDK2014年2月</description> + </item> + <item> + <title> + 物件名:富士見町3(武蔵大和駅) 2480万円</title> + <link>https://suumo.jp/jj/bukken/shosai/JJ010FJ100/?ar=030&bs=021&nc=93382908&ta=13&sc=13213</link> + <description> + 東京都東村山市富士見町32480万円西武多摩湖線武蔵大和徒歩16分144.59m&sup2;95.32m&sup2;3LDK2001年10月</description> + </item> + <item> + <title> + 物件名:富士見町3(武蔵大和駅) 2480万円</title> + <link>https://suumo.jp/jj/bukken/shosai/JJ010FJ100/?ar=030&bs=021&nc=93376878&ta=13&sc=13213</link> + <description> + 東京都東村山市富士見町32480万円西武多摩湖線武蔵大和徒歩16分144.59m&sup2;(43.73坪)(登記)95.32m&sup2;(28.83坪)(登記)3LDK2001年10月</description> + </item> + <item> + <title> + 物件名:狭山(新狭山駅) 2480万円</title> + <link>https://suumo.jp/jj/bukken/shosai/JJ010FJ100/?ar=030&bs=021&nc=93373359&ta=11&sc=11215</link> + <description> + 埼玉県狭山市狭山2480万円西武新宿線新狭山徒歩22分193.71m&sup2;(58.59坪)(登記)101.02m&sup2;(30.55坪)(登記)4LDK2017年8月</description> + </item> + <item> + <title> + 物件名:散田町2(西八王子駅) 2480万円</title> + <link>https://suumo.jp/jj/bukken/shosai/JJ010FJ100/?ar=030&bs=021&nc=93380220&ta=13&sc=13201</link> + <description> + 東京都八王子市散田町22480万円JR中央線西八王子徒歩10分158m&sup2;109.3m&sup2;5LDK+S(納戸)1979年10月</description> + </item> + <item> + <title> + 物件名:下貝塚2(市川大野駅) 2499万円</title> + <link>https://suumo.jp/jj/bukken/shosai/JJ010FJ100/?ar=030&bs=021&nc=93381924&ta=12&sc=12203</link> + <description> + 千葉県市川市下貝塚22499万円JR武蔵野線市川大野徒歩27分117.14m&sup2;(35.43坪)(登記)82.65m&sup2;(25.00坪)(登記)3LDK2003年6月</description> + </item> + <item> + <title> + 物件名:青葉町2 2499万円</title> + <link>https://suumo.jp/jj/bukken/shosai/JJ010FJ100/?ar=030&bs=021&nc=93321520&ta=13&sc=13213</link> + <description> + 東京都東村山市青葉町22499万円西武新宿線東村山バス24分106.28m&sup2;(登記)84.45m&sup2;(登記)3LDK2005年11月</description> + </item> + <item> + <title> + 物件名:落合南2 2599万円</title> + <link>https://suumo.jp/jj/bukken/shosai/JJ010FJ100/?ar=030&bs=021&nc=93377715&ta=14&sc=14218</link> + <description> + 神奈川県綾瀬市落合南22599万円小田急江ノ島線長後バス13分132m&sup2;(登記)162.05m&sup2;(登記)5LDK+2S(納戸)1994年1月</description> + </item> + <item> + <title> + 物件名:青柳3(矢川駅) 2649万円</title> + <link>https://suumo.jp/jj/bukken/shosai/JJ010FJ100/?ar=030&bs=021&nc=93392540&ta=13&sc=13215</link> + <description> + 東京都国立市青柳32649万円JR南武線矢川徒歩18分49.74m&sup2;78.19m&sup2;3LDK2000年2月</description> + </item> + <item> + <title> + 物件名:笹下2(上大岡駅) 2680万円</title> + <link>https://suumo.jp/jj/bukken/shosai/JJ010FJ100/?ar=030&bs=021&nc=93366398&ta=14&sc=14111</link> + <description> + 神奈川県横浜市港南区笹下22680万円京急本線上大岡徒歩23分107.66m&sup2;(32.56坪)(登記)80.96m&sup2;(24.49坪)(登記)3LDK1985年4月</description> + </item> + <item> + <title> + 物件名:【初掲載】総武線市川駅から徒歩たったの8分 築浅中古住宅が登場!!</title> + <link>https://suumo.jp/jj/bukken/shosai/JJ010FJ100/?ar=030&bs=021&nc=93385960&ta=12&sc=12203</link> + <description> + 千葉県市川市市川南32680万円JR総武線市川徒歩7分49.75m&sup2;(登記)48.74m&sup2;(登記)2DK2010年10月</description> + </item> + <item> + <title> + 物件名:砂川町7(玉川上水駅) 2699万円</title> + <link>https://suumo.jp/jj/bukken/shosai/JJ010FJ100/?ar=030&bs=021&nc=93392636&ta=13&sc=13202</link> + <description> + 東京都立川市砂川町72699万円西武拝島線玉川上水徒歩11分115.58m&sup2;90.6m&sup2;4LDK2002年10月</description> + </item> + <item> + <title> + 物件名:今宿中古戸建</title> + <link>https://suumo.jp/jj/bukken/shosai/JJ010FJ100/?ar=030&bs=021&nc=93356356&ta=14&sc=14207</link> + <description> + 神奈川県茅ヶ崎市今宿2780万円JR東海道本線茅ヶ崎バス7分137.27m&sup2;(登記)147.38m&sup2;(登記)5LDK+2S(納戸)1994年12月</description> + </item> + </channel> +</rss> diff --git a/src/test/resources/youtube.xml b/src/test/resources/youtube.xml @@ -0,0 +1,696 @@ +<?xml version="1.0" encoding="UTF-8"?> +<feed xmlns:yt="http://www.youtube.com/xml/schemas/2015" xmlns:media="http://search.yahoo.com/mrss/" xmlns="http://www.w3.org/2005/Atom"> + <link rel="self" href="http://www.youtube.com/feeds/videos.xml?channel_id=UCcziTK2NKeWtWQ6kB5tmQ8Q"/> + <id>yt:channel:UCcziTK2NKeWtWQ6kB5tmQ8Q</id> + <yt:channelId>UCcziTK2NKeWtWQ6kB5tmQ8Q</yt:channelId> + <title>e-penser</title> + <link rel="alternate" href="https://www.youtube.com/channel/UCcziTK2NKeWtWQ6kB5tmQ8Q"/> + <author> + <name>e-penser</name> + <uri>https://www.youtube.com/channel/UCcziTK2NKeWtWQ6kB5tmQ8Q</uri> + </author> + <published>2013-08-26T19:18:50+00:00</published> + <entry> + <id>yt:video:hJe5MDMWOaU</id> + <yt:videoId>hJe5MDMWOaU</yt:videoId> + <yt:channelId>UCcziTK2NKeWtWQ6kB5tmQ8Q</yt:channelId> + <title>Les trous noirs (1/2) - 48 - e-penser</title> + <link rel="alternate" href="https://www.youtube.com/watch?v=hJe5MDMWOaU"/> + <author> + <name>e-penser</name> + <uri>https://www.youtube.com/channel/UCcziTK2NKeWtWQ6kB5tmQ8Q</uri> + </author> + <published>2020-01-23T08:09:43+00:00</published> + <updated>2020-01-23T12:14:22+00:00</updated> + <media:group> + <media:title>Les trous noirs (1/2) - 48 - e-penser</media:title> + <media:content url="https://www.youtube.com/v/hJe5MDMWOaU?version=3" type="application/x-shockwave-flash" width="640" height="390"/> + <media:thumbnail url="https://i1.ytimg.com/vi/hJe5MDMWOaU/hqdefault.jpg" width="480" height="360"/> + <media:description>Abonnez-vous : http://bit.ly/epenserabonnement +et matez AREL3 : http://bit.ly/arel3s1 + +http://tipeee.com/e-penser +http://twitter.com/epenser +https://www.instagram.com/e_penser + +2019 © Loading, please wait... + +Liens : +- Lettre de Cavendish : https://www.jstor.org/stable/106576 + +- Exposition du système du monde vol. 2 (Laplace) : https://gallica.bnf.fr/ark:/12148/bpt6k1050382f/ + +Liens vidéos : +- ouvrir une porte : https://www.youtube.com/watch?v=vN0uNlJNzhI +- relativité générale : https://www.youtube.com/watch?v=6JLgWX9iqgo +- le big bang : https://www.youtube.com/watch?v=Ky0Qcjs2uiA&list=PLGPWPtcc-r815tk1FswuVYD-KkaXqmbgY +- Kelvin : https://www.youtube.com/watch?v=Rs7WkNV8nAA +- Catastrophe ultraviolette : https://www.youtube.com/watch?v=nxHH-uq_0Sg +- le Sense of Wonder, Nucléosynthèse stellaire : https://www.youtube.com/watch?v=64f2MEM67Qg + +Autres vidéos sur les trous noirs : +- Kurzgesagt : https://www.youtube.com/watch?v=e-P5IFTqB98 +- Kurzgesagt : https://www.youtube.com/watch?v=udFxKZRyQt4 +- Veritasium : https://www.youtube.com/watch?v=zUyH3XhpLTo +- Science4All : https://www.youtube.com/watch?v=Yc7snENoFqs + +Musique de l'épisode : La grande table productions +site : http://grandetableproductions.com +playlist youtube : https://www.youtube.com/channel/UC_fQgXy-p_vjjEbuv1ggBEQ/feed +soundcloud : https://soundcloud.com/la-grande-table +spotify : https://open.spotify.com/album/67eNgaOEcO41EcsX8vISYK</media:description> + <media:community> + <media:starRating count="5609" average="4.96" min="1" max="5"/> + <media:statistics views="39193"/> + </media:community> + </media:group> + </entry> + <entry> + <id>yt:video:Sk7Ia2Lsuak</id> + <yt:videoId>Sk7Ia2Lsuak</yt:videoId> + <yt:channelId>UCcziTK2NKeWtWQ6kB5tmQ8Q</yt:channelId> + <title>La migraine est une horreur - 47 - e-penser</title> + <link rel="alternate" href="https://www.youtube.com/watch?v=Sk7Ia2Lsuak"/> + <author> + <name>e-penser</name> + <uri>https://www.youtube.com/channel/UCcziTK2NKeWtWQ6kB5tmQ8Q</uri> + </author> + <published>2020-01-09T15:51:58+00:00</published> + <updated>2020-01-14T23:44:47+00:00</updated> + <media:group> + <media:title>La migraine est une horreur - 47 - e-penser</media:title> + <media:content url="https://www.youtube.com/v/Sk7Ia2Lsuak?version=3" type="application/x-shockwave-flash" width="640" height="390"/> + <media:thumbnail url="https://i4.ytimg.com/vi/Sk7Ia2Lsuak/hqdefault.jpg" width="480" height="360"/> + <media:description>La migraine n'est sans doute pas ce que vous croyez, et il faut que vous le sachiez. + +Abonnez-vous : http://bit.ly/epenserabonnement +Et matez AREL3 : http://bit.ly/arel3s1 + +Liens : +- la gueule de bois : https://www.youtube.com/watch?v=pwUAPlYiFgk +- le cerveau : https://www.youtube.com/watch?v=7GiQuG2S26Q +- la migraine (DTC) : https://www.youtube.com/watch?v=ruY6guNSnaw + +http://twitch.tv/epenser +http://tipeee.com/e-penser - http://e-penser.com - http://youtube.com/epenser1 - http://youtube.com/epenser2 - http://facebook.com/epenser - http://twitter.com/epenser - https://www.instagram.com/e_penser + +© e-penser + +Musique de l'épisode : La grande table productions +site : http://grandetableproductions.com +spotify : https://open.spotify.com/album/67eNgaOEcO41EcsX8vISYK +soundcloud : https://soundcloud.com/la-grande-table</media:description> + <media:community> + <media:starRating count="22929" average="4.97" min="1" max="5"/> + <media:statistics views="237606"/> + </media:community> + </media:group> + </entry> + <entry> + <id>yt:video:1Bn50keR6UY</id> + <yt:videoId>1Bn50keR6UY</yt:videoId> + <yt:channelId>UCcziTK2NKeWtWQ6kB5tmQ8Q</yt:channelId> + <title>Le mathématicien nul de l'Indiana - Flash 09 - e-penser</title> + <link rel="alternate" href="https://www.youtube.com/watch?v=1Bn50keR6UY"/> + <author> + <name>e-penser</name> + <uri>https://www.youtube.com/channel/UCcziTK2NKeWtWQ6kB5tmQ8Q</uri> + </author> + <published>2019-12-27T17:00:25+00:00</published> + <updated>2020-01-16T22:09:24+00:00</updated> + <media:group> + <media:title>Le mathématicien nul de l'Indiana - Flash 09 - e-penser</media:title> + <media:content url="https://www.youtube.com/v/1Bn50keR6UY?version=3" type="application/x-shockwave-flash" width="640" height="390"/> + <media:thumbnail url="https://i2.ytimg.com/vi/1Bn50keR6UY/hqdefault.jpg" width="480" height="360"/> + <media:description>Abonnez-vous : http://bit.ly/epenserabonnement +et matez AREL3 : http://bit.ly/arel3s1 +Le jour où PI a valu exactement 3,2 dans l'Indiana, aux USA. + +http://tipeee.com/e-penser +http://twitter.com/epenser +https://www.instagram.com/e_penser + +2019 © Loading, please wait... + +Liens : +- la vidéo sur l'irrationnalité de la diagonale du carré de 1 de côté : https://www.youtube.com/watch?v=xVHvgINaLok +- la proposition de loi : http://www.indianalegalarchive.com/journal/2015/3/14/legislating-pi +- à propos de la proposition de loi : https://link.springer.com/chapter/10.1007/978-1-4757-4217-6_26 +- la publication de l'American Mathematical Monthly : https://www.jstor.org/stable/2971093 +- sur la quadrature du cercle : https://www.youtube.com/watch?v=VgNUGxBDxUA + +Musique de l'épisode : La grande table productions +site : http://grandetableproductions.com +spotify : https://open.spotify.com/album/67eNgaOEcO41EcsX8vISYK +soundcloud : https://soundcloud.com/la-grande-table</media:description> + <media:community> + <media:starRating count="18747" average="4.93" min="1" max="5"/> + <media:statistics views="226724"/> + </media:community> + </media:group> + </entry> + <entry> + <id>yt:video:Ktvfe4BfVbQ</id> + <yt:videoId>Ktvfe4BfVbQ</yt:videoId> + <yt:channelId>UCcziTK2NKeWtWQ6kB5tmQ8Q</yt:channelId> + <title>#08 Le toucher, partie 2 - Les sens humains - e-penser</title> + <link rel="alternate" href="https://www.youtube.com/watch?v=Ktvfe4BfVbQ"/> + <author> + <name>e-penser</name> + <uri>https://www.youtube.com/channel/UCcziTK2NKeWtWQ6kB5tmQ8Q</uri> + </author> + <published>2019-12-19T15:53:31+00:00</published> + <updated>2019-12-22T10:00:20+00:00</updated> + <media:group> + <media:title>#08 Le toucher, partie 2 - Les sens humains - e-penser</media:title> + <media:content url="https://www.youtube.com/v/Ktvfe4BfVbQ?version=3" type="application/x-shockwave-flash" width="640" height="390"/> + <media:thumbnail url="https://i4.ytimg.com/vi/Ktvfe4BfVbQ/hqdefault.jpg" width="480" height="360"/> + <media:description>Abonnez-vous : http://bit.ly/epenserabonnement +Et matez AREL3 : http://bit.ly/arel3s1 + +Les vidéos sur les sens : https://www.youtube.com/watch?v=6XQhUJ5fqMk&list=PLGPWPtcc-r826iYRn9D0rZUDliNsK2XJP + +http://tipeee.com/e-penser - http://e-penser.com - http://youtube.com/epenser1 - http://youtube.com/epenser2 - http://facebook.com/epenser - http://twitter.com/epenser - https://www.instagram.com/e_penser +http://twitch.tv/epenser + +© e-penser + +Musique de l'épisode : La grande table productions +site : http://grandetableproductions.com +playlist youtube : https://www.youtube.com/channel/UC_fQgXy-p_vjjEbuv1ggBEQ/feed +soundcloud : https://soundcloud.com/la-grande-table +spotify : https://open.spotify.com/album/67eNgaOEcO41EcsX8vISYK + +Illustrations 3D : Complete anatomy 2019 © 3d4medical +http://3d4medical.com + +Illustrations complémentaires @ Shutterstock</media:description> + <media:community> + <media:starRating count="12132" average="4.97" min="1" max="5"/> + <media:statistics views="132157"/> + </media:community> + </media:group> + </entry> + <entry> + <id>yt:video:uVdkBetn0rs</id> + <yt:videoId>uVdkBetn0rs</yt:videoId> + <yt:channelId>UCcziTK2NKeWtWQ6kB5tmQ8Q</yt:channelId> + <title>#07 Le toucher, partie 1 - Les sens humains - e-penser</title> + <link rel="alternate" href="https://www.youtube.com/watch?v=uVdkBetn0rs"/> + <author> + <name>e-penser</name> + <uri>https://www.youtube.com/channel/UCcziTK2NKeWtWQ6kB5tmQ8Q</uri> + </author> + <published>2019-12-12T15:51:18+00:00</published> + <updated>2019-12-14T23:30:39+00:00</updated> + <media:group> + <media:title>#07 Le toucher, partie 1 - Les sens humains - e-penser</media:title> + <media:content url="https://www.youtube.com/v/uVdkBetn0rs?version=3" type="application/x-shockwave-flash" width="640" height="390"/> + <media:thumbnail url="https://i2.ytimg.com/vi/uVdkBetn0rs/hqdefault.jpg" width="480" height="360"/> + <media:description>ERRATUM : Dans la vidéo, je dis que le poids moyen de la peau est de 3kg chez l'homme et 5kg chez la femme, c'est l'inverse en réalité. +Abonnez-vous : http://bit.ly/epenserabonnement +Et matez AREL3 : http://bit.ly/arel3s1 + +Les vidéos sur les sens : https://www.youtube.com/watch?v=6XQhUJ5fqMk&list=PLGPWPtcc-r826iYRn9D0rZUDliNsK2XJP + +http://tipeee.com/e-penser - http://e-penser.com - http://youtube.com/epenser1 - http://youtube.com/epenser2 - http://facebook.com/epenser - http://twitter.com/epenser - https://www.instagram.com/e_penser +http://twitch.tv/epenser + +© e-penser + +Etude sur la surdité des félins liée aux mélanocytes : https://www.ncbi.nlm.nih.gov/pubmed/23122176 + +Musique de l'épisode : La grande table productions +site : http://grandetableproductions.com +playlist youtube : https://www.youtube.com/channel/UC_fQgXy-p_vjjEbuv1ggBEQ/feed +soundcloud : https://soundcloud.com/la-grande-table +spotify : https://open.spotify.com/album/67eNgaOEcO41EcsX8vISYK + +Illustrations 3D : Complete anatomy 2019 © 3d4medical +http://3d4medical.com</media:description> + <media:community> + <media:starRating count="15412" average="4.94" min="1" max="5"/> + <media:statistics views="184386"/> + </media:community> + </media:group> + </entry> + <entry> + <id>yt:video:BWzy0HmtDUY</id> + <yt:videoId>BWzy0HmtDUY</yt:videoId> + <yt:channelId>UCcziTK2NKeWtWQ6kB5tmQ8Q</yt:channelId> + <title>Pourquoi le réveil nous réveille-t-il - quickie 18 - e-penser</title> + <link rel="alternate" href="https://www.youtube.com/watch?v=BWzy0HmtDUY"/> + <author> + <name>e-penser</name> + <uri>https://www.youtube.com/channel/UCcziTK2NKeWtWQ6kB5tmQ8Q</uri> + </author> + <published>2019-12-06T15:55:25+00:00</published> + <updated>2019-12-09T05:34:38+00:00</updated> + <media:group> + <media:title>Pourquoi le réveil nous réveille-t-il - quickie 18 - e-penser</media:title> + <media:content url="https://www.youtube.com/v/BWzy0HmtDUY?version=3" type="application/x-shockwave-flash" width="640" height="390"/> + <media:thumbnail url="https://i3.ytimg.com/vi/BWzy0HmtDUY/hqdefault.jpg" width="480" height="360"/> + <media:description>Abonnez-vous : http://bit.ly/epenserabonnement +et matez AREL3 : http://bit.ly/arel3s1 + +Vidéo réalisée en partenariat avec la société Saint-Gobain, tournée en partie au +Domolab (© Les Sismo / © Encore Heureux Architectes) + +http://tipeee.com/e-penser +http://twitter.com/epenser +https://www.instagram.com/e_penser + +2019 © Loading, please wait... + +Liens : +- https://www.nature.com/articles/s41562-018-0502-5 +- https://presse.ademe.fr/2016/06/etude-cout-social-du-bruit-en-france.html +- vidéo sur l'ouïe : https://www.youtu.be/i2bKuw011nk +- vidéo sur le cerveau : https://youtu.be/7GiQuG2S26Q + +Musique de l'épisode : La grande table productions +site : http://grandetableproductions.com +playlist youtube : https://www.youtube.com/channel/UC_fQgXy-p_vjjEbuv1ggBEQ/feed +soundcloud : https://soundcloud.com/la-grande-table</media:description> + <media:community> + <media:starRating count="26488" average="4.91" min="1" max="5"/> + <media:statistics views="344852"/> + </media:community> + </media:group> + </entry> + <entry> + <id>yt:video:dQxmdIBU2Ok</id> + <yt:videoId>dQxmdIBU2Ok</yt:videoId> + <yt:channelId>UCcziTK2NKeWtWQ6kB5tmQ8Q</yt:channelId> + <title>Le Big Bang (3/3) - 46 - e-penser</title> + <link rel="alternate" href="https://www.youtube.com/watch?v=dQxmdIBU2Ok"/> + <author> + <name>e-penser</name> + <uri>https://www.youtube.com/channel/UCcziTK2NKeWtWQ6kB5tmQ8Q</uri> + </author> + <published>2019-11-28T16:54:10+00:00</published> + <updated>2020-01-21T22:45:23+00:00</updated> + <media:group> + <media:title>Le Big Bang (3/3) - 46 - e-penser</media:title> + <media:content url="https://www.youtube.com/v/dQxmdIBU2Ok?version=3" type="application/x-shockwave-flash" width="640" height="390"/> + <media:thumbnail url="https://i1.ytimg.com/vi/dQxmdIBU2Ok/hqdefault.jpg" width="480" height="360"/> + <media:description>Abonnez-vous : http://bit.ly/epenserabonnement +et matez AREL3 : http://bit.ly/arel3s1 + +EPISODES PRECEDENTS : +Partie 1 - https://youtu.be/Ky0Qcjs2uiA +Partie 2 - https://youtu.be/bAljyiRHUms + +http://tipeee.com/e-penser +http://twitter.com/epenser +https://www.instagram.com/e_penser + +2019 © Loading, please wait... + +quelques articles de Linde : +- https://web.stanford.edu/~alinde/1984.pdf +- https://arxiv.org/pdf/astro-ph/9601004.pdf +- https://arxiv.org/pdf/gr-qc/9306035.pdf +- https://arxiv.org/pdf/0705.0164.pdf + +la mise en évidence des ondes gravitationnelles GW150914 : https://www.gw-openscience.org/events/GW150914/ + +la fin des résultats de BICEP2 : https://www.nature.com/news/gravitational-waves-discovery-now-officially-dead-1.16830 + +Liens vidéos : +- interview Alan Guth par Physics girl : https://www.youtube.com/watch?v=Vn985KyDpb8 +- vidéo sur l'univers plat (origine de la recherche d'Alan Guth sur l'inflation cosmique) : https://www.youtube.com/watch?v=MTUsOWtxKKA +- la relativité générale : https://www.youtube.com/watch?v=6JLgWX9iqgo + +Musique de l'épisode : La grande table productions +site : http://grandetableproductions.com +playlist youtube : https://www.youtube.com/channel/UC_fQgXy-p_vjjEbuv1ggBEQ/feed +soundcloud : https://soundcloud.com/la-grande-table</media:description> + <media:community> + <media:starRating count="41223" average="4.97" min="1" max="5"/> + <media:statistics views="325879"/> + </media:community> + </media:group> + </entry> + <entry> + <id>yt:video:bAljyiRHUms</id> + <yt:videoId>bAljyiRHUms</yt:videoId> + <yt:channelId>UCcziTK2NKeWtWQ6kB5tmQ8Q</yt:channelId> + <title>Le Big Bang (2/3) - 45 - e-penser (LIRE LA DESCRIPTION)</title> + <link rel="alternate" href="https://www.youtube.com/watch?v=bAljyiRHUms"/> + <author> + <name>e-penser</name> + <uri>https://www.youtube.com/channel/UCcziTK2NKeWtWQ6kB5tmQ8Q</uri> + </author> + <published>2019-11-21T18:32:29+00:00</published> + <updated>2020-01-21T22:45:03+00:00</updated> + <media:group> + <media:title>Le Big Bang (2/3) - 45 - e-penser (LIRE LA DESCRIPTION)</media:title> + <media:content url="https://www.youtube.com/v/bAljyiRHUms?version=3" type="application/x-shockwave-flash" width="640" height="390"/> + <media:thumbnail url="https://i3.ytimg.com/vi/bAljyiRHUms/hqdefault.jpg" width="480" height="360"/> + <media:description>ATTENTION, j'ai fait une petite boulette dans la vidéo : l'azote se note N pour Nitrogène, et pas Natrium comme je le dis dans la vidéo (Natrium, c'est Na, c'est à dire le sodium). Je vais aller pleurer dans un coin, maintenant... + +Abonnez-vous : http://bit.ly/epenserabonnement +et matez AREL3 : http://bit.ly/arel3s1 + +EPISODE PRECEDENT : https://youtu.be/Ky0Qcjs2uiA + +http://tipeee.com/e-penser +http://twitter.com/epenser +https://www.instagram.com/e_penser + +2019 © Loading, please wait... + +Lettre d'Einstein à propos de Friedmann : http://alberteinstein.info/vufind1/Digital/EAR000034024#page/1/mode/1up +Article de George Lemaître : http://articles.adsabs.harvard.edu//full/1927ASSB...47...49L +Article de Georges Lemaître (traduction anglaise) : http://articles.adsabs.harvard.edu/pdf/1931MNRAS..91..483L +Vote de l'UAI sur le nom de la loi de Hubble-Lemaître : https://www.iau.org/news/pressreleases/detail/iau1812/ +Article Alpha-Beta-Gamma : https://journals.aps.org/pr/pdf/10.1103/PhysRev.73.803 +Sur Ralph Alpher : https://www.nationalmedals.org/laureates/ralph-a-alpher +https://arxiv.org/ftp/arxiv/papers/1411/1411.0172.pdf + +Liens vidéos : +- 10 choses sur l'univers : + - https://youtu.be/ItffNZtUYXA + - https://youtu.be/uu1DIgEMa7c +- catastrophe ultraviolette : https://youtu.be/nxHH-uq_0Sg +- Kelvin : https://youtu.be/Rs7WkNV8nAA + +Musique de l'épisode : La grande table productions +site : http://grandetableproductions.com +playlist youtube : https://www.youtube.com/channel/UC_fQgXy-p_vjjEbuv1ggBEQ/feed +soundcloud : https://soundcloud.com/la-grande-table</media:description> + <media:community> + <media:starRating count="26344" average="4.95" min="1" max="5"/> + <media:statistics views="368967"/> + </media:community> + </media:group> + </entry> + <entry> + <id>yt:video:Ky0Qcjs2uiA</id> + <yt:videoId>Ky0Qcjs2uiA</yt:videoId> + <yt:channelId>UCcziTK2NKeWtWQ6kB5tmQ8Q</yt:channelId> + <title>Le Big Bang (1/3)- 44 - e-penser</title> + <link rel="alternate" href="https://www.youtube.com/watch?v=Ky0Qcjs2uiA"/> + <author> + <name>e-penser</name> + <uri>https://www.youtube.com/channel/UCcziTK2NKeWtWQ6kB5tmQ8Q</uri> + </author> + <published>2019-11-15T17:41:41+00:00</published> + <updated>2020-01-21T22:44:47+00:00</updated> + <media:group> + <media:title>Le Big Bang (1/3)- 44 - e-penser</media:title> + <media:content url="https://www.youtube.com/v/Ky0Qcjs2uiA?version=3" type="application/x-shockwave-flash" width="640" height="390"/> + <media:thumbnail url="https://i4.ytimg.com/vi/Ky0Qcjs2uiA/hqdefault.jpg" width="480" height="360"/> + <media:description>Abonnez-vous : http://bit.ly/epenserabonnement +et matez AREL3 : http://bit.ly/arel3s1 + +http://tipeee.com/e-penser +http://twitter.com/epenser +https://www.instagram.com/e_penser + +2019 © Loading, please wait... + +lettre d'Einstein à Ehrenfest : https://einsteinpapers.press.princeton.edu/vol8a-doc/458 +Publication de Christian Doppler : https://archive.org/details/ueberdasfarbigel00doppuoft +à propos de la citation de Giordano Bruno (son "De la causa..." n'est pas disponible publiquement sur le web, a priori) : https://www.academia.edu/4697789/ +Liens vidéos : +- relativité restreinte : https://www.youtube.com/watch?v=KX9QSjv0Ib0 +- relativité générale : https://www.youtube.com/watch?v=6JLgWX9iqgo +- ouverture d'une porte : https://www.youtube.com/watch?v=vN0uNlJNzhI +- l'atome : https://www.youtube.com/watch?v=3iIpl6tvT7A +- Mendeleiev : https://www.youtube.com/watch?v=0lNuTSz6KVM + + +Autres vidéos sur le Big Bang : +- https://www.youtube.com/watch?v=YwuJxomtO1o +- https://www.youtube.com/watch?v=tq6be-CZJ3w +- https://www.youtube.com/watch?v=9B7Ix2VQEGo +- https://www.youtube.com/watch?v=IGCVTSQw7WU +- https://www.youtube.com/watch?v=wNDGgL73ihY +- https://www.youtube.com/watch?v=7LV6jJoggPk +- https://www.youtube.com/watch?v=X9D43ECoYWg + +Musique de l'épisode : La grande table productions +site : http://grandetableproductions.com +playlist youtube : https://www.youtube.com/channel/UC_fQgXy-p_vjjEbuv1ggBEQ/feed +soundcloud : https://soundcloud.com/la-grande-table</media:description> + <media:community> + <media:starRating count="39457" average="4.84" min="1" max="5"/> + <media:statistics views="562429"/> + </media:community> + </media:group> + </entry> + <entry> + <id>yt:video:28rkT6TSFlg</id> + <yt:videoId>28rkT6TSFlg</yt:videoId> + <yt:channelId>UCcziTK2NKeWtWQ6kB5tmQ8Q</yt:channelId> + <title>La contrefaçon - quickie 17 - e-penser</title> + <link rel="alternate" href="https://www.youtube.com/watch?v=28rkT6TSFlg"/> + <author> + <name>e-penser</name> + <uri>https://www.youtube.com/channel/UCcziTK2NKeWtWQ6kB5tmQ8Q</uri> + </author> + <published>2019-06-23T17:02:43+00:00</published> + <updated>2019-10-03T12:12:21+00:00</updated> + <media:group> + <media:title>La contrefaçon - quickie 17 - e-penser</media:title> + <media:content url="https://www.youtube.com/v/28rkT6TSFlg?version=3" type="application/x-shockwave-flash" width="640" height="390"/> + <media:thumbnail url="https://i3.ytimg.com/vi/28rkT6TSFlg/hqdefault.jpg" width="480" height="360"/> + <media:description>Abonnez-vous : http://bit.ly/epenserabonnement +Et matez AREL3 : http://bit.ly/arel3s1 +Nouveau décor (qui résonne encore un peu, mais les travaux d'acoustique sont encore en cours) pour cette nouvelle vidéo. + +http://twitch.tv/epenser +http://tipeee.com/e-penser - http://e-penser.com - http://youtube.com/epenser1 - http://youtube.com/epenser2 - http://facebook.com/epenser - http://twitter.com/epenser - https://www.instagram.com/e_penser + +© e-penser + +Liens : +- https://birdabroad.wordpress.com/2011/07/23/fake-apple-store-update-with-video/ +- http://www.unifab.com +- https://www.unifab.com/rapport-analyses/ +- http://www.iracm.com/ + +Musique de l'épisode : La grande table productions +site : http://grandetableproductions.com +playlist youtube : https://www.youtube.com/channel/UC_fQgXy-p_vjjEbuv1ggBEQ/feed +soundcloud : https://soundcloud.com/la-grande-table</media:description> + <media:community> + <media:starRating count="40063" average="4.65" min="1" max="5"/> + <media:statistics views="571662"/> + </media:community> + </media:group> + </entry> + <entry> + <id>yt:video:Zb3LixrUmDw</id> + <yt:videoId>Zb3LixrUmDw</yt:videoId> + <yt:channelId>UCcziTK2NKeWtWQ6kB5tmQ8Q</yt:channelId> + <title>La SNCF peut-elle être ponctuelle ? - 43 - e-penser</title> + <link rel="alternate" href="https://www.youtube.com/watch?v=Zb3LixrUmDw"/> + <author> + <name>e-penser</name> + <uri>https://www.youtube.com/channel/UCcziTK2NKeWtWQ6kB5tmQ8Q</uri> + </author> + <published>2019-05-17T16:38:11+00:00</published> + <updated>2019-05-21T01:57:12+00:00</updated> + <media:group> + <media:title>La SNCF peut-elle être ponctuelle ? - 43 - e-penser</media:title> + <media:content url="https://www.youtube.com/v/Zb3LixrUmDw?version=3" type="application/x-shockwave-flash" width="640" height="390"/> + <media:thumbnail url="https://i3.ytimg.com/vi/Zb3LixrUmDw/hqdefault.jpg" width="480" height="360"/> + <media:description>La SNCF a pour réputation auprès du grand public d'accumuler les retards, particulièrement en Ile de France. Pourtant, compte tenu de la densité du trafic, le système devrait être continuellement à terre. Comment la SNCF résout-elle ce paradoxe, et mérite-t-elle cette réputation ? +#SNCF #viveletrain http://viveletrain.sncf + +Abonnez-vous : http://bit.ly/epenserabonnement +Et matez AREL3 : http://bit.ly/arel3s1 + +http://twitch.tv/epenser +http://tipeee.com/e-penser - http://e-penser.com - http://youtube.com/epenser1 - http://youtube.com/epenser2 - http://facebook.com/epenser - http://twitter.com/epenser - https://www.instagram.com/e_penser + +© e-penser + +Liens : +- La chaîne de Jérôme Béron : https://www.youtube.com/user/twizyrider +- Ma chaîne twitch : http://twitch.tv/epenser +- le blog de la ligne H : https://maligneh.transilien.com/ +- le site du transilien : https://www.transilien.com/ +- le moteur de recherche open de la SNCF : https://www.sncf.com/fr/engagements/transparence/opendata + +Musique de l'épisode : La grande table productions +site : http://grandetableproductions.com +playlist youtube : https://www.youtube.com/channel/UC_fQgXy-p_vjjEbuv1ggBEQ/feed +soundcloud : https://soundcloud.com/la-grande-table</media:description> + <media:community> + <media:starRating count="35428" average="4.88" min="1" max="5"/> + <media:statistics views="617959"/> + </media:community> + </media:group> + </entry> + <entry> + <id>yt:video:YjWXowvRRo4</id> + <yt:videoId>YjWXowvRRo4</yt:videoId> + <yt:channelId>UCcziTK2NKeWtWQ6kB5tmQ8Q</yt:channelId> + <title>Interviews Alita:Battle Angel chez Weta Digital (ft. Rosa Salazar)</title> + <link rel="alternate" href="https://www.youtube.com/watch?v=YjWXowvRRo4"/> + <author> + <name>e-penser</name> + <uri>https://www.youtube.com/channel/UCcziTK2NKeWtWQ6kB5tmQ8Q</uri> + </author> + <published>2019-02-11T18:15:37+00:00</published> + <updated>2020-01-16T05:58:28+00:00</updated> + <media:group> + <media:title>Interviews Alita:Battle Angel chez Weta Digital (ft. Rosa Salazar)</media:title> + <media:content url="https://www.youtube.com/v/YjWXowvRRo4?version=3" type="application/x-shockwave-flash" width="640" height="390"/> + <media:thumbnail url="https://i2.ytimg.com/vi/YjWXowvRRo4/hqdefault.jpg" width="480" height="360"/> + <media:description>Echange avec ceux qui ont rendu le film Alita:Battle Angel possible technologiquement. Pensez à partager la vidéo, et abonnez-vous : http://bit.ly/epenserabonnement +Ah, et matez AREL3, aussi : http://bit.ly/arel3s1 + +http://tipeee.com/e-penser - http://e-penser.com - http://youtube.com/epenser1 - http://youtube.com/epenser2 - http://facebook.com/epenser - http://twitter.com/epenser - https://www.instagram.com/e_penser +http://twitch.tv/epenser + +© e-penser + +La bande-annonce du film Alita: Battle Angel (sortie le 13 février 2019) : https://www.youtube.com/watch?v=75QXYNz5Q8o + +Un Q&A avec Jon Landau et Robert Rodriguez (et moi, coucou) : https://www.youtube.com/watch?v=qSZyXDXmaUQ +(c'est la version courte...peut-être bientôt une version plus longue) + +Merci à Jon Landau, Robert Rodriguez, Rosa Salazar, Joe Letteri, Eric Saindon, Mike Cozens, 20th Century Fox, Weta Digital, et les dizaines de personnes qu'on ne voit pas nécessairement à l'image mais qui ont rendu cette vidéo possible.</media:description> + <media:community> + <media:starRating count="6485" average="4.76" min="1" max="5"/> + <media:statistics views="139803"/> + </media:community> + </media:group> + </entry> + <entry> + <id>yt:video:p3joYSEMkwE</id> + <yt:videoId>p3joYSEMkwE</yt:videoId> + <yt:channelId>UCcziTK2NKeWtWQ6kB5tmQ8Q</yt:channelId> + <title>Je teste la performance capture (feat. Robert Rodriguez)</title> + <link rel="alternate" href="https://www.youtube.com/watch?v=p3joYSEMkwE"/> + <author> + <name>e-penser</name> + <uri>https://www.youtube.com/channel/UCcziTK2NKeWtWQ6kB5tmQ8Q</uri> + </author> + <published>2019-02-05T16:47:36+00:00</published> + <updated>2020-01-18T00:53:45+00:00</updated> + <media:group> + <media:title>Je teste la performance capture (feat. Robert Rodriguez)</media:title> + <media:content url="https://www.youtube.com/v/p3joYSEMkwE?version=3" type="application/x-shockwave-flash" width="640" height="390"/> + <media:thumbnail url="https://i1.ytimg.com/vi/p3joYSEMkwE/hqdefault.jpg" width="480" height="360"/> + <media:description>Pensez à partager la vidéo, et abonnez-vous : http://bit.ly/epenserabonnement +Ah, et matez AREL3, aussi : http://bit.ly/arel3s1 + +http://tipeee.com/e-penser - http://e-penser.com - http://youtube.com/epenser1 - http://youtube.com/epenser2 - http://facebook.com/epenser - http://twitter.com/epenser - https://www.instagram.com/e_penser +http://twitch.tv/epenser + +© e-penser + +La chanson "Swan Song" de Dua Lipa : +- https://warner.link/dualipaswansongclipjt +- https://warner.link/dualipaswansongir + +La bande-annonce du film Alita: Battle Angel (sortie le 13 février 2019) : https://www.youtube.com/watch?v=75QXYNz5Q8o + +Un Q&A avec Jon Landau et Robert Rodriguez (et moi, coucou) : https://www.youtube.com/watch?v=qSZyXDXmaUQ +(c'est la version courte...peut-être bientôt une version plus longue) + +Musique de l'épisode : La grande table productions +site : http://grandetableproductions.com +playlist youtube : https://www.youtube.com/channel/UC_fQgXy-p_vjjEbuv1ggBEQ/feed +soundcloud : https://soundcloud.com/la-grande-table + +Merci à Jon Landau, Robert Rodriguez, Rosa Salazar, 20th Century Fox, Weta Digital, et les dizaines de personnes qu'on ne voit pas nécessairement à l'image mais qui ont rendu cette vidéo possible.</media:description> + <media:community> + <media:starRating count="23690" average="4.96" min="1" max="5"/> + <media:statistics views="222941"/> + </media:community> + </media:group> + </entry> + <entry> + <id>yt:video:VQjutS-MBUc</id> + <yt:videoId>VQjutS-MBUc</yt:videoId> + <yt:channelId>UCcziTK2NKeWtWQ6kB5tmQ8Q</yt:channelId> + <title>#06 L'odorat, partie 2 - Les sens humains - e-penser</title> + <link rel="alternate" href="https://www.youtube.com/watch?v=VQjutS-MBUc"/> + <author> + <name>e-penser</name> + <uri>https://www.youtube.com/channel/UCcziTK2NKeWtWQ6kB5tmQ8Q</uri> + </author> + <published>2019-01-20T14:05:57+00:00</published> + <updated>2020-01-20T02:55:50+00:00</updated> + <media:group> + <media:title>#06 L'odorat, partie 2 - Les sens humains - e-penser</media:title> + <media:content url="https://www.youtube.com/v/VQjutS-MBUc?version=3" type="application/x-shockwave-flash" width="640" height="390"/> + <media:thumbnail url="https://i3.ytimg.com/vi/VQjutS-MBUc/hqdefault.jpg" width="480" height="360"/> + <media:description>La première partie ici : https://youtu.be/dJ0fo5Tp06A +Abonnez-vous : http://bit.ly/epenserabonnement +Et matez AREL3 : http://bit.ly/arel3s1 + +http://tipeee.com/e-penser - http://e-penser.com - http://youtube.com/epenser1 - http://youtube.com/epenser2 - http://facebook.com/epenser - http://twitter.com/epenser - https://www.instagram.com/e_penser +http://twitch.tv/epenser + +© e-penser + +Merci à Hardisk : +- https://www.youtube.com/channel/UCA6rJQnIFgQOcjO-xtNV7lg + +Liens : +- https://www.youtube.com/watch?v=6XQhUJ5fqMk&list=PLGPWPtcc-r826iYRn9D0rZUDliNsK2XJP +- https://www.youtube.com/watch?v=9wxd8MVVBpU + +Musique de l'épisode : La grande table productions +site : http://grandetableproductions.com +playlist youtube : https://www.youtube.com/channel/UC_fQgXy-p_vjjEbuv1ggBEQ/feed +soundcloud : https://soundcloud.com/la-grande-table + +Illustrations 3D : Complete anatomy 2019 © 3d4medical +http://3d4medical.com</media:description> + <media:community> + <media:starRating count="10381" average="4.96" min="1" max="5"/> + <media:statistics views="121455"/> + </media:community> + </media:group> + </entry> + <entry> + <id>yt:video:dJ0fo5Tp06A</id> + <yt:videoId>dJ0fo5Tp06A</yt:videoId> + <yt:channelId>UCcziTK2NKeWtWQ6kB5tmQ8Q</yt:channelId> + <title>#05 L'odorat, partie 1 - Les sens humains - e-penser</title> + <link rel="alternate" href="https://www.youtube.com/watch?v=dJ0fo5Tp06A"/> + <author> + <name>e-penser</name> + <uri>https://www.youtube.com/channel/UCcziTK2NKeWtWQ6kB5tmQ8Q</uri> + </author> + <published>2019-01-13T12:45:28+00:00</published> + <updated>2020-01-22T23:37:35+00:00</updated> + <media:group> + <media:title>#05 L'odorat, partie 1 - Les sens humains - e-penser</media:title> + <media:content url="https://www.youtube.com/v/dJ0fo5Tp06A?version=3" type="application/x-shockwave-flash" width="640" height="390"/> + <media:thumbnail url="https://i1.ytimg.com/vi/dJ0fo5Tp06A/hqdefault.jpg" width="480" height="360"/> + <media:description>Abonnez-vous : http://bit.ly/epenserabonnement +Et matez AREL3 : http://bit.ly/arel3s1 + +http://tipeee.com/e-penser - http://e-penser.com - http://youtube.com/epenser1 - http://youtube.com/epenser2 - http://facebook.com/epenser - http://twitter.com/epenser - https://www.instagram.com/e_penser +http://twitch.tv/epenser + +© e-penser + +Liens : +- https://www.youtube.com/watch?v=6XQhUJ5fqMk&list=PLGPWPtcc-r826iYRn9D0rZUDliNsK2XJP +- https://www.youtube.com/watch?v=hLavUzFWITY&vl=fr + +Musique de l'épisode : La grande table productions +site : http://grandetableproductions.com +playlist youtube : https://www.youtube.com/channel/UC_fQgXy-p_vjjEbuv1ggBEQ/feed +soundcloud : https://soundcloud.com/la-grande-table + +Illustrations 3D : Complete anatomy 2019 © 3d4medical +http://3d4medical.com</media:description> + <media:community> + <media:starRating count="10545" average="4.94" min="1" max="5"/> + <media:statistics views="177458"/> + </media:community> + </media:group> + </entry> +</feed> +\ No newline at end of file