extract images from article in Reader
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using PropertyChanged;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace PocketSharp.Models
|
||||
{
|
||||
@@ -17,6 +18,14 @@ namespace PocketSharp.Models
|
||||
/// </value>
|
||||
public string Content { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the images.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The images.
|
||||
/// </value>
|
||||
public List<PocketArticleImage> Images { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the title.
|
||||
/// </summary>
|
||||
@@ -0,0 +1,36 @@
|
||||
using PropertyChanged;
|
||||
using System;
|
||||
|
||||
namespace PocketSharp.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Article image
|
||||
/// </summary>
|
||||
[ImplementPropertyChanged]
|
||||
public class PocketArticleImage
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the URI.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The URI.
|
||||
/// </value>
|
||||
public Uri Uri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the title.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The title.
|
||||
/// </value>
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the alternative text.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The alternative text.
|
||||
/// </value>
|
||||
public string AlternativeText { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
using PocketSharp.Models;
|
||||
using PocketSharp.Ports.NReadability;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace PocketSharp
|
||||
{
|
||||
@@ -107,9 +110,25 @@ namespace PocketSharp
|
||||
// process/transcode HTML
|
||||
TranscodingResult transcodingResult = transcoder.Transcode(transcodingInput);
|
||||
|
||||
// get images from article
|
||||
List<PocketArticleImage> images = transcodingResult.RawDocument.GetElementsByTagName("img").Select<XElement, PocketArticleImage>(image =>
|
||||
{
|
||||
Uri imageUri;
|
||||
Uri.TryCreate(image.GetAttributeValue("src", null), UriKind.Absolute, out imageUri);
|
||||
|
||||
return new PocketArticleImage()
|
||||
{
|
||||
Uri = imageUri,
|
||||
Title = image.GetAttributeValue("title", null),
|
||||
AlternativeText = image.GetAttributeValue("alt", null)
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
// create article
|
||||
return new PocketArticle()
|
||||
{
|
||||
Content = transcodingResult.ExtractedContent,
|
||||
Images = images,
|
||||
Title = transcodingResult.ExtractedTitle,
|
||||
NextPage = transcodingResult.NextPageUrl != null ? new Uri(transcodingResult.NextPageUrl, UriKind.Absolute) : null
|
||||
};
|
||||
|
||||
@@ -42,7 +42,8 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="IPocketReader.cs" />
|
||||
<Compile Include="PocketArticle.cs" />
|
||||
<Compile Include="Models\PocketArticle.cs" />
|
||||
<Compile Include="Models\PocketArticleImage.cs" />
|
||||
<Compile Include="PocketReader.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -39,6 +39,24 @@ namespace PocketSharp.Tests
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public async Task ReadArticleWithImagesTest()
|
||||
{
|
||||
PocketArticle result = await reader.Read(new Uri("https://hacks.mozilla.org/2013/12/application-layout-with-css3-flexible-box-module/"));
|
||||
Assert.True(result.Images.Count >= 3);
|
||||
Assert.True(result.Images[0].Uri.ToString().StartsWith("https://hacks.mozilla.org"));
|
||||
Assert.True(result.Images[1].Uri.ToString().EndsWith(".gif"));
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public async Task ReadArticleWithNoImagesTest()
|
||||
{
|
||||
PocketArticle result = await reader.Read(new Uri("http://getpocket.com/hits/awards/2013/"));
|
||||
Assert.True(result.Images == null || result.Images.Count < 1);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public async Task ReadArticleWithInvalidUriTest()
|
||||
{
|
||||
|
||||
@@ -44,7 +44,6 @@
|
||||
<Compile Include="Components\Statistics.cs" />
|
||||
<Compile Include="IPocketClient.cs" />
|
||||
<Compile Include="Models\Parameters\RegisterParameters.cs" />
|
||||
<Compile Include="Models\PocketArticle.cs" />
|
||||
<Compile Include="Models\PocketBoolean.cs" />
|
||||
<Compile Include="Models\PocketLimits.cs" />
|
||||
<Compile Include="Models\PocketStatistics.cs" />
|
||||
|
||||
@@ -22,9 +22,9 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Reflection;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
|
||||
@@ -36,15 +36,15 @@ namespace PocketSharp.Ports.NReadability
|
||||
public class NReadabilityTranscoder
|
||||
{
|
||||
#region Nested types
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Used in FindNextPageLink
|
||||
/// </summary>
|
||||
private class LinkData
|
||||
{
|
||||
public float Score;
|
||||
public string LinkText;
|
||||
public string LinkHref;
|
||||
public float Score;
|
||||
public string LinkText;
|
||||
public string LinkHref;
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -102,7 +102,7 @@ namespace PocketSharp.Ports.NReadability
|
||||
|
||||
private static readonly Regex _UnlikelyCandidatesRegex = new Regex("combx|comment|community|disqus|extra|foot|header|menu|remark|rss|shoutbox|sidebar|side|sponsor|ad-break|agegate|pagination|pager|popup|tweet|twitter", RegexOptions.IgnoreCase);
|
||||
private static readonly Regex _OkMaybeItsACandidateRegex = new Regex("and|article|body|column|main|shadow", RegexOptions.IgnoreCase);
|
||||
private static readonly Regex _PositiveWeightRegex = new Regex("article|body|content|entry|hentry|main|page|pagination|post|text|blog|story", RegexOptions.IgnoreCase);
|
||||
private static readonly Regex _PositiveWeightRegex = new Regex("article|body|content|entry|hentry|main|page|pagination|post|text|blog|story", RegexOptions.IgnoreCase);
|
||||
private static readonly Regex _NegativeWeightRegex = new Regex("combx|comment|com-|contact|foot|footer|footnote|masthead|media|meta|outbrain|promo|related|scroll|shoutbox|sidebar|side|sponsor|shopping|tags|tool|widget", RegexOptions.IgnoreCase);
|
||||
private static readonly Regex _NegativeLinkParentRegex = new Regex("(stories|articles|news|documents|posts|notes|series|historie|artykuly|artykuły|wpisy|dokumenty|serie|geschichten|erzählungen|erzahlungen)", RegexOptions.IgnoreCase);
|
||||
private static readonly Regex _Extraneous = new Regex("print|archive|comment|discuss|e[-]?mail|share|reply|all|login|sign|single|also", RegexOptions.IgnoreCase);
|
||||
@@ -118,7 +118,7 @@ namespace PocketSharp.Ports.NReadability
|
||||
private static readonly Regex _ArticleTitleDashRegex2 = new Regex("(.*)[\\|\\-] .*");
|
||||
private static readonly Regex _ArticleTitleDashRegex3 = new Regex("[^\\|\\-]*[\\|\\-](.*)");
|
||||
private static readonly Regex _ArticleTitleColonRegex1 = new Regex(".*:(.*)");
|
||||
private static readonly Regex _ArticleTitleColonRegex2 = new Regex("[^:]*[:](.*)");
|
||||
private static readonly Regex _ArticleTitleColonRegex2 = new Regex("[^:]*[:](.*)");
|
||||
private static readonly Regex _NextLink = new Regex(@"(next|weiter|continue|dalej|następna|nastepna>([^\|]|$)|�([^\|]|$))", RegexOptions.IgnoreCase);
|
||||
private static readonly Regex _NextStoryLink = new Regex("(story|article|news|document|post|note|series|historia|artykul|artykuł|wpis|dokument|seria|geschichte|erzählung|erzahlung|artikel|serie)", RegexOptions.IgnoreCase);
|
||||
private static readonly Regex _PrevLink = new Regex("(prev|earl|[^b]old|new|wstecz|poprzednia|<|�)", RegexOptions.IgnoreCase);
|
||||
@@ -148,7 +148,7 @@ namespace PocketSharp.Ports.NReadability
|
||||
private readonly ReadingStyle _readingStyle;
|
||||
private readonly ReadingSize _readingSize;
|
||||
private readonly ReadingMargin _readingMargin;
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper instance fields
|
||||
@@ -190,7 +190,7 @@ namespace PocketSharp.Ports.NReadability
|
||||
|
||||
_sgmlDomBuilder = new SgmlDomBuilder();
|
||||
_sgmlDomSerializer = new SgmlDomSerializer();
|
||||
_elementsScores = new Dictionary<XElement, float>();
|
||||
_elementsScores = new Dictionary<XElement, float>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -374,7 +374,7 @@ namespace PocketSharp.Ports.NReadability
|
||||
_dontStripUnlikelys = true;
|
||||
|
||||
return TranscodeToXml(htmlContent, url, out mainContentExtracted, out extractedTitle, out nextPageUrl);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_dontStripUnlikelys = false;
|
||||
@@ -444,7 +444,7 @@ namespace PocketSharp.Ports.NReadability
|
||||
|
||||
/* If the leftovers of the URL after removing the base URL don't contain any digits, it's certainly not a next page link. */
|
||||
string linkHrefLeftover = linkHref.Replace(articleBaseUrl, "");
|
||||
|
||||
|
||||
if (!Regex.IsMatch(linkHrefLeftover, @"\d"))
|
||||
{
|
||||
continue;
|
||||
@@ -453,7 +453,7 @@ namespace PocketSharp.Ports.NReadability
|
||||
if (!possiblePagesByLink.Keys.Contains(linkHref))
|
||||
{
|
||||
possiblePagesByLink[linkHref] = new LinkData { Score = 0, LinkHref = linkHref, LinkText = linkText };
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
possiblePagesByLink[linkHref].LinkText += " | " + linkText;
|
||||
@@ -541,7 +541,7 @@ namespace PocketSharp.Ports.NReadability
|
||||
linkObj.Score += 12.5f;
|
||||
positiveDescendantMatch = true;
|
||||
}
|
||||
|
||||
|
||||
if (!negativeDescendantMatch && _PrevLink.IsMatch(descendantData))
|
||||
{
|
||||
linkObj.Score -= 100;
|
||||
@@ -604,7 +604,7 @@ namespace PocketSharp.Ports.NReadability
|
||||
|
||||
if (topPage != null)
|
||||
{
|
||||
string nextHref = Regex.Replace(topPage.LinkHref, @"\/$", "");
|
||||
string nextHref = Regex.Replace(topPage.LinkHref, @"\/$", "");
|
||||
var nextHrefUri = new Uri(new Uri(articleBaseUrl), nextHref);
|
||||
|
||||
return nextHrefUri.OriginalString;
|
||||
@@ -612,7 +612,7 @@ namespace PocketSharp.Ports.NReadability
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Find a cleaned up version of the current URL, to use for comparing links for possible next-pageyness.
|
||||
/// </summary>
|
||||
@@ -626,13 +626,13 @@ namespace PocketSharp.Ports.NReadability
|
||||
}
|
||||
|
||||
string protocol = urlUri.Scheme;
|
||||
string hostname = urlUri.Host;
|
||||
string hostname = urlUri.Host;
|
||||
string noUrlParams = urlUri.AbsolutePath + "/";
|
||||
List<string> urlSlashes = noUrlParams.Split('/').Reverse().ToList();
|
||||
var cleanedSegments = new List<string>();
|
||||
int slashLen = urlSlashes.Count();
|
||||
|
||||
for (int i = 0; i < slashLen; i++)
|
||||
for (int i = 0; i < slashLen; i++)
|
||||
{
|
||||
string segment = urlSlashes[i];
|
||||
|
||||
@@ -656,10 +656,10 @@ namespace PocketSharp.Ports.NReadability
|
||||
{
|
||||
segment = segment.Replace(",00", "");
|
||||
}
|
||||
|
||||
|
||||
/* If our first or second segment has anything looking like a page number, remove it. */
|
||||
var pageNumRegex = new Regex("((_|-)?p[a-z]*|(_|-))[0-9]{1,2}$", RegexOptions.IgnoreCase);
|
||||
|
||||
|
||||
if (pageNumRegex.IsMatch(segment) && ((i == 1) || (i == 0)))
|
||||
{
|
||||
segment = pageNumRegex.Replace(segment, "");
|
||||
@@ -752,7 +752,7 @@ namespace PocketSharp.Ports.NReadability
|
||||
|
||||
/* Remove all anchors. */
|
||||
elementsToRemove.Clear();
|
||||
|
||||
|
||||
IEnumerable<XElement> anchorElements =
|
||||
rootElement.GetElementsByTagName("a")
|
||||
.Where(aElement => aElement.Attribute("name") != null && aElement.Attribute("href") == null);
|
||||
@@ -929,63 +929,63 @@ namespace PocketSharp.Ports.NReadability
|
||||
|
||||
new ElementsTraverser(
|
||||
element =>
|
||||
{
|
||||
string elementName = GetElementName(element);
|
||||
|
||||
/* Remove unlikely candidates. */
|
||||
string unlikelyMatchString = element.GetClass() + " " + element.GetId();
|
||||
|
||||
if (unlikelyMatchString.Length > 0
|
||||
&& !"body".Equals(elementName, StringComparison.OrdinalIgnoreCase)
|
||||
&& !"a".Equals(elementName, StringComparison.OrdinalIgnoreCase)
|
||||
&& _UnlikelyCandidatesRegex.IsMatch(unlikelyMatchString)
|
||||
&& !_OkMaybeItsACandidateRegex.IsMatch(unlikelyMatchString))
|
||||
{
|
||||
string elementName = GetElementName(element);
|
||||
XElement parentElement = element.Parent;
|
||||
|
||||
/* Remove unlikely candidates. */
|
||||
string unlikelyMatchString = element.GetClass() + " " + element.GetId();
|
||||
|
||||
if (unlikelyMatchString.Length > 0
|
||||
&& !"body".Equals(elementName, StringComparison.OrdinalIgnoreCase)
|
||||
&& !"a".Equals(elementName, StringComparison.OrdinalIgnoreCase)
|
||||
&& _UnlikelyCandidatesRegex.IsMatch(unlikelyMatchString)
|
||||
&& !_OkMaybeItsACandidateRegex.IsMatch(unlikelyMatchString))
|
||||
if (parentElement != null)
|
||||
{
|
||||
XElement parentElement = element.Parent;
|
||||
|
||||
if (parentElement != null)
|
||||
{
|
||||
element.Remove();
|
||||
}
|
||||
|
||||
// element has been removed - we can go to the next one
|
||||
return;
|
||||
element.Remove();
|
||||
}
|
||||
|
||||
/* Turn all divs that don't have children block level elements into p's or replace text nodes within the div with p's. */
|
||||
if ("div".Equals(elementName, StringComparison.OrdinalIgnoreCase))
|
||||
// element has been removed - we can go to the next one
|
||||
return;
|
||||
}
|
||||
|
||||
/* Turn all divs that don't have children block level elements into p's or replace text nodes within the div with p's. */
|
||||
if ("div".Equals(elementName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!_DivToPElementsRegex.IsMatch(element.GetInnerHtml()))
|
||||
{
|
||||
if (!_DivToPElementsRegex.IsMatch(element.GetInnerHtml()))
|
||||
{
|
||||
// no block elements inside - change to p
|
||||
SetElementName(element, "p");
|
||||
}
|
||||
else
|
||||
{
|
||||
// replace text nodes with p's (experimental)
|
||||
new ChildNodesTraverser(
|
||||
childNode =>
|
||||
{
|
||||
if (childNode.NodeType != XmlNodeType.Text
|
||||
|| GetInnerText(childNode).Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
XElement paraElement = new XElement("p");
|
||||
|
||||
// note that we're not using GetInnerText() here; instead we're getting raw InnerText to preserve whitespaces
|
||||
paraElement.SetInnerHtml(((XText)childNode).Value);
|
||||
|
||||
paraElement.SetClass(ReadabilityStyledCssClass);
|
||||
paraElement.SetStyle("display: inline;");
|
||||
|
||||
childNode.ReplaceWith(paraElement);
|
||||
}
|
||||
).Traverse(element);
|
||||
}
|
||||
// no block elements inside - change to p
|
||||
SetElementName(element, "p");
|
||||
}
|
||||
}).Traverse(rootElement);
|
||||
else
|
||||
{
|
||||
// replace text nodes with p's (experimental)
|
||||
new ChildNodesTraverser(
|
||||
childNode =>
|
||||
{
|
||||
if (childNode.NodeType != XmlNodeType.Text
|
||||
|| GetInnerText(childNode).Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
XElement paraElement = new XElement("p");
|
||||
|
||||
// note that we're not using GetInnerText() here; instead we're getting raw InnerText to preserve whitespaces
|
||||
paraElement.SetInnerHtml(((XText)childNode).Value);
|
||||
|
||||
paraElement.SetClass(ReadabilityStyledCssClass);
|
||||
paraElement.SetStyle("display: inline;");
|
||||
|
||||
childNode.ReplaceWith(paraElement);
|
||||
}
|
||||
).Traverse(element);
|
||||
}
|
||||
}
|
||||
}).Traverse(rootElement);
|
||||
}
|
||||
|
||||
internal void CollapseRedundantParagraphDivs(XDocument document)
|
||||
@@ -994,31 +994,31 @@ namespace PocketSharp.Ports.NReadability
|
||||
|
||||
new ElementsTraverser(
|
||||
element =>
|
||||
{
|
||||
string elementName = GetElementName(element);
|
||||
|
||||
if ("div".Equals(elementName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string elementName = GetElementName(element);
|
||||
XNode childNode = element.Nodes().SingleOrNone();
|
||||
|
||||
if ("div".Equals(elementName, StringComparison.OrdinalIgnoreCase))
|
||||
if (childNode != null)
|
||||
{
|
||||
XNode childNode = element.Nodes().SingleOrNone();
|
||||
XElement childElement = childNode as XElement;
|
||||
|
||||
if (childNode != null)
|
||||
if (childElement != null && "p".Equals(GetElementName(childElement), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
XElement childElement = childNode as XElement;
|
||||
// we have a div with a single child element that is a paragraph - let's remove the div and attach the paragraph to the div's parent
|
||||
XElement parentElement = element.Parent;
|
||||
|
||||
if (childElement != null && "p".Equals(GetElementName(childElement), StringComparison.OrdinalIgnoreCase))
|
||||
if (parentElement != null)
|
||||
{
|
||||
// we have a div with a single child element that is a paragraph - let's remove the div and attach the paragraph to the div's parent
|
||||
XElement parentElement = element.Parent;
|
||||
|
||||
if (parentElement != null)
|
||||
{
|
||||
element.AddBeforeSelf(childElement);
|
||||
element.Remove();
|
||||
}
|
||||
element.AddBeforeSelf(childElement);
|
||||
element.Remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}).Traverse(rootElement);
|
||||
}
|
||||
}).Traverse(rootElement);
|
||||
}
|
||||
|
||||
internal IEnumerable<XElement> FindCandidatesForArticleContent(XDocument document, string articleContentElementHint = null)
|
||||
@@ -1137,7 +1137,7 @@ namespace PocketSharp.Ports.NReadability
|
||||
Math.Max(
|
||||
_MaxSiblingScoreTreshold,
|
||||
_SiblingScoreTresholdCoefficient * topCandidateElementScore);
|
||||
|
||||
|
||||
string topCandidateClass = topCandidateElement.GetClass();
|
||||
|
||||
// iterate through the sibling elements and decide whether append them
|
||||
@@ -1224,7 +1224,8 @@ namespace PocketSharp.Ports.NReadability
|
||||
Clean(articleContentElement, "form");
|
||||
Clean(articleContentElement, "object");
|
||||
|
||||
if (articleContentElement.GetElementsByTagName("h1").Count() == 1) {
|
||||
if (articleContentElement.GetElementsByTagName("h1").Count() == 1)
|
||||
{
|
||||
Clean(articleContentElement, "h1");
|
||||
}
|
||||
|
||||
@@ -1516,17 +1517,17 @@ namespace PocketSharp.Ports.NReadability
|
||||
{
|
||||
new ElementsTraverser(
|
||||
element =>
|
||||
{
|
||||
string elementClass = element.GetClass();
|
||||
|
||||
if (elementClass.Contains(ReadabilityStyledCssClass))
|
||||
{
|
||||
string elementClass = element.GetClass();
|
||||
// don't remove the style if that's we who have styled this element
|
||||
return;
|
||||
}
|
||||
|
||||
if (elementClass.Contains(ReadabilityStyledCssClass))
|
||||
{
|
||||
// don't remove the style if that's we who have styled this element
|
||||
return;
|
||||
}
|
||||
|
||||
element.SetStyle(null);
|
||||
}).Traverse(rootElement);
|
||||
element.SetStyle(null);
|
||||
}).Traverse(rootElement);
|
||||
}
|
||||
|
||||
internal string GetUserStyleClass(string prefix, String enumStr)
|
||||
@@ -1650,7 +1651,7 @@ namespace PocketSharp.Ports.NReadability
|
||||
return url;
|
||||
}
|
||||
|
||||
Uri baseUri;
|
||||
Uri baseUri;
|
||||
|
||||
if (!Uri.TryCreate(articleUrl, UriKind.Absolute, out baseUri))
|
||||
{
|
||||
@@ -1659,8 +1660,8 @@ namespace PocketSharp.Ports.NReadability
|
||||
|
||||
/* If the link is simply a query string, then simply attach it to the original URL */
|
||||
if (url.StartsWith("?"))
|
||||
{
|
||||
return baseUri.Scheme + "://" + baseUri.Host + baseUri.AbsolutePath + url;
|
||||
{
|
||||
return baseUri.Scheme + "://" + baseUri.Host + baseUri.AbsolutePath + url;
|
||||
}
|
||||
|
||||
Uri absoluteUri;
|
||||
|
||||
Reference in New Issue
Block a user