I'm looking for a way for FeedAPI to delete items as soon as they fall off the feed, so that the stored items are exactly the same as what the feed was when last read.

We use Vocus, which lets our media officers arbitrarily pick news stories to be published on an RSS feed that we post to our site (in a block), so we need old stories to immediately drop off whenever they choose something new. On the other hand, we can't really use "Delete Items Older Than," because some stories may stay on the feed for a week or two.

But the same might be true of a flickr group or the feed for a del.icio.us tag.

Here's a solution, but it's not pretty, so perhaps you can think of a better way in a future version. The problem is the "unique" functionality is designed to compare a new item to previously read items in the database, but not the other way around. So at a high level (above the processor), it's impossible to identify items that aren't on the current feed and delete them. That's why this is built into feedapi_aggregator.module:

1) Extend _feedapi_aggregator_unique to optionally return the matching fiid (for internal use)
2) Extend _feedapi_aggregator_expire to pre-scan items from the feed and delete ones in the database that don't match.

--- feedapi_aggregator.module   30 Jul 2008 15:39:37 -0000      1.2
+++ feedapi_aggregator.module   30 Jul 2008 16:01:14 -0000
@@ -1,5 +1,5 @@
 <?php
-// $Id: feedapi_aggregator.module,v 1.2 2008/07/30 15:39:37 cvsroot Exp $
+// $Id: feedapi_aggregator.module,v 1.1 2008/05/27 18:04:23 devseed Exp $

 /**
  * @abstract This module emulates aggregator module with the feedapi framework.
@@ -194,6 +194,12 @@
         '#default_value' => 3,
         '#options' => drupal_map_assoc(array(2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)),
       );
+         $form['delete_missing'] = array(
+               '#type' => 'checkbox',
+               '#title' => t('Delete missing feed items'),
+               '#description' => t('If checked, previously read feed items will be removed when the feed is refreshed, if they are no longer in the feed (even if they haven\'t expired).'),
+               '#default_value' => 1,
+         );
       $categories_result = db_query('SELECT cid, title FROM {feedapi_aggregator_category}');
       $categories = array();
       while ($category = db_fetch_object($categories_result)) {
@@ -510,7 +516,7 @@
 /**
  * Is this feed item created?
  */
-function _feedapi_aggregator_unique($feed_item, $feed_nid, $settings = array()) {
+function _feedapi_aggregator_unique($feed_item, $feed_nid, $settings = array(), $return_id=FALSE) {
   $entry = FALSE;
   if ($feed_item->options->guid) {
     $entry = db_fetch_object(db_query("SELECT iid FROM {feedapi_aggregator_item} WHERE feed_nid = %d AND guid = '%s'", $feed_nid, $feed_item->options->guid));
@@ -522,6 +528,11 @@
   else {
     $entry = db_fetch_object(db_query("SELECT iid FROM {feedapi_aggregator_item} WHERE feed_nid = %d AND title = '%s'", $feed_nid, $feed_item->title));
   }
+
+  if( $return_id ) {
+    return is_object($entry) ? $entry->iid : null;
+  }
+
   return is_object($entry) ? FALSE : TRUE;
 }

@@ -552,6 +563,25 @@
       $count++;
     }
   }
+
+  $processor_settings = $settings['processors']['feedapi_aggregator'];
+  if( $processor_settings['delete_missing'] ) {
+    $items_to_keep = array();
+       foreach( $feed->items as $index => $item) {
+         if( $iid = module_invoke('feedapi_aggregator', 'feedapi_item', 'unique', $item, $feed->nid, $processor_settings, TRUE) ) {
+           $items_to_keep[] = $iid;
+         }
+       }
+
+       if( $items_to_keep ) {
+         $result = db_query('SELECT * FROM {feedapi_aggregator_item} WHERE feed_nid=%d AND iid NOT IN (%s)', $feed->nid, implode(',', $items_to_keep));
+         while( $item = db_fetch_object($result) ) {
+           $item->fiid = $item->iid;
+               feedapi_expire_item($feed, $item);
+               $count++;
+         }
+       }
+  }
   return $count;
 }

I also had to change feedapi.module so that new items are read before the call to feedapi_expire, so the latter can see new items:

--- feedapi.module      27 May 2008 18:04:23 -0000      1.1
+++ feedapi.module      30 Jul 2008 16:00:30 -0000
@@ -1125,16 +1125,17 @@
   }
   $settings = feedapi_get_settings(NULL, $feed->nid);

-  // Step 1: Force processors to delete old items and determine the max. create elements.
-  $counter['expired'] = feedapi_expire($feed);
-
-  // Step 2: Get feed.
+  // Step 1: Get feed.
   $nid = $feed->nid;
   $hash_old = $feed->hash;
   $feed = _feedapi_call_parsers($feed, $feed->parsers, $feed->half_done);
   if (is_object($feed)) {
     $feed->hash = md5(serialize($feed->items));
   }
+
+  // Step 2: Force processors to delete old items and determine the max. create elements.
+  $counter['expired'] = feedapi_expire($feed);
+
   // Step 3: See, whether feed has been modified.
   if ($feed === FALSE || $hash_old == $feed->hash) {
     // Updated the checked field in any case.

Comments

Jomel’s picture

Subscribing. This would be extremely useful, as we want to use RSS to mirror content from another site. I haven't yet had time to see if the patch applies against 1.4...

doublejosh’s picture

Version: 5.x-1.2 » 5.x-1.5
Component: Code » Code feedapi_node

This would be great. I'd love a way to set a max number of feed items for a given feed. Or even just a way to set a max number to be fetched at cron or on refresh.

Possible already???

aron novak’s picture

This is not possible at the moment. Also unfortunately I don't plan to add new features to the 5.x branch. If someone step up with a patch, i happily review it.

doublejosh’s picture

Are maxes and or throttling planned for V6?

I'm about to start on a feed aggregation project, that might enable me to spend time on this! :)

quickcel’s picture

Subscribing

quickcel’s picture

StatusFileSize
new1.95 KB
new1.61 KB
new15.27 KB

I've created a patch for the 6.x branch that I think provides the functionality that the original poster mentioned (thanks to therzog for some of the code).

Basically, there is a new option under the FeedApi Processor settings to delete old nodes that drop off the feed (see attachment for screenshot). If that box is checked then when a feed is refreshed (manually or through cron) any items that have fallen off the feed will be deleted.

It's working well for a couple of feeds I'm using at the moment, but definitely needs some testing and review.

levelos’s picture

Noticed that the changes to feedapi.module in the last patch only apply to the node processor, which might not be used in all cases. I don't like the idea of storing feed items as nodes and am working on a lighter weight processor, and am having the same "dropped" feeds issue.

levelos’s picture

Version: 5.x-1.5 » 6.x-1.7-beta2

This is the code I ended up writing within my own processor to make this work. It's called during the expire operation. Works well in preliminary testing.

   if ($settings['processors']['feedapi_toolbox']['delete_missing']) {
    $guids_to_keep = array();
    foreach ($feed->parsers as $parser) {
      $result = module_invoke($parser, 'feedapi_feed', 'parse', $feed);
      foreach($result->items as $item){
        if(!empty($item->options->guid)){
          $guids_to_keep[] = "'" . $item->options->guid . "'";
        }
      }
    }  
    if (!empty($guids_to_keep)) {
      $result = db_query("SELECT * FROM {feedapi_toolbox_item} WHERE nid = %d AND guid NOT IN (".implode(", ", $guids_to_keep).")", $feed->nid);
      while ($item = db_fetch_object($result)) {
        // We callback feedapi for deleting
        feedapi_expire_item($feed, $item);
      }
    }  
  }

NOTE: Code snipped updated since original post.

chrism2671’s picture

Has any of this been committed to the dev branch? It seems like mirroring the RSS feed should be a relatively trivial operation...

quickcel’s picture

@chrism2671 - I do not believe so

DavidWhite’s picture

Subscribe

alex_b’s picture

StatusFileSize
new1.34 KB

We've recently had a similar requirement. I'm way to paranoid to actually delete items when they are not found on the feed (what happens if the feed blips?) - so I opted for unpublishing them. I wrote it as separate processor and called it FeedAPI garbage collector :)

Here's the module. If somebody wants to run with it ad maintain it on d.o. as separate project, I'm all for it.

CheezItMan’s picture

I have another problem i that when I hit refresh on the feed to start creating nodes from a google calendar. I get a blank white screen.

pdcarto’s picture

#12 works for me. Simple and clean.

philpro’s picture

Sweet! it works. and if i change line 45 in "feedapi_gc.module" from

db_query('UPDATE {node} SET status = 0 WHERE nid = %d', $feed_item->nid);

to

node_delete($feed_item->nid);

It deletes those offending nodes. Thanks for this module.

johnmunro’s picture

#12 not working for me.

The 'FeedAPI Garbage Collector' is appearing in the content type as a Processor correctly, but it fails to appear in the actual feed item content. Only the 'FeedAPI Node' processor is showing and no items are ever unpublished (or deleted with #15's code)

Copyfight’s picture

subscribing

quickcel’s picture

If you manually refresh the feed will it unpublish items?

If you can manually refresh the feeds and they work OK, but they don't update with a cron run you may want to check out this discussion: http://drupal.org/node/580508

johnmunro’s picture

Status: Active » Needs review

I can now understand why there is not feedapi GC processor shown when editing the feed item (not content type) as there is no function feedapi_gc_feedapi_settings_form. This seems ok in that there are no settings required.

Strangely however, the function feedapi_gc_feedapi_after_refresh never runs on a manual or cron refresh for me (I'm using Firebug for Drupal to check this).

...

I have now found out why. The module_feedapi_after_refresh only gets to run if there has been a change in the actual feed. In my testing, my test machine had run cron after some items were removed from the feed but before adding the gc module.

Making a small change in the feed kicked it into gear.

This extra module approach seems the most elegant and bug free approach to solve this issue. Maybe it can be added to the feedapi standard add ons?

I suggest the following changes:
Change feedapi_gc.info from:

name = FeedAPI Garbage Collector
description = This FeedAPI processor unpublishes feed item nodes that are not present on the feed.
core = 6.x

To:

name = FeedAPI Garbage Collector
description = This FeedAPI processor unpublishes feed item nodes that are not present on the feed.
dependencies[] = feedapi
package = FeedAPI Add On
core = 6.x

AND add the following to the end of the README.txt

After installation, the feed must actually change before the garbage collector 
will run.
johnmunro’s picture

StatusFileSize
new1.41 KB

Attaching update version of alex_b's module for review

johnmunro’s picture

Status: Needs review » Needs work
StatusFileSize
new1.49 KB

Whoops, the above attachment says 'items _deleted_' rather than 'items unpublished'

For flexibility, also inserted this code at line 45 of feedapi_gc.module

      // TO DO: change the following into an admin preference
      // Uncomment the next line to delete rather than unpublish
      //node_delete($feed_item->nid);
      // Comment the next line to delete rather than unpublish
johnmunro’s picture

Another issue has arisen with the message feedback to the user.

    drupal_set_message(t('!checked items checked, !removed items removed.', array('!checked' => $checked, '!removed' => $removed)));

is good and helpful unless it is a cron run, in which case the message is shown to the next Anonym visitor, confusing them. Therefore something like this code is needed:

  if (!$cron) {
    drupal_set_message(t('!checked items checked, !removed items removed.', array('!checked' => $checked, '!removed' => $removed)));
  }

However, there is no access to the $cron variable in the calling function from feedapi.module _feedapi_invoke_refresh so what to do?? As I see it there needs to be an API change to pass this variable in the hook_feedapi_after_refresh.

Namely, feedapi.module line ~1278:

  foreach (module_implements('feedapi_after_refresh') as $module) {
    $func = $module .'_feedapi_after_refresh';
    $func($feed);
  }

Change to:

  foreach (module_implements('feedapi_after_refresh') as $module) {
    $func = $module .'_feedapi_after_refresh';
    $func($feed, $cron);
  }

Or can someone think of a less disruptive method. No feedback message is one way...

amir simantov’s picture

Subscribing.

khapin’s picture

Subscribing

imclean’s picture

#21 johnmunro, I agree with the code comment about it being an admin setting, perhaps on a per-feed basis.

volocuga’s picture

How to use this module?
After the module was enabled all my nodes come as "unpublished",but I need it could be in "published" state by default and change to "unpublished" after feed's items will removed.

johnmunro’s picture

If the module is working correctly it would only unpublish all nodes if the feed was inaccessible. (Which is something that probably needs work on as this is not a good behaviour)

SeanBannister’s picture

sub

oldscoutmaster’s picture

Subscribing.

netentropy’s picture

There is an actual setting for this, FeedAPI is suppose to refresh to match the existing feed. Actually I think it used to when I used to use it.

Not sure why it doesn't now. But this is a very needed feature.

You could always run cron to delete feed items then refresh them.

alison’s picture

subscribing

andrewspringman’s picture

subscribing

aron novak’s picture

Status: Needs work » Needs review
StatusFileSize
new1.49 KB

I did couple of changes to make this tiny module more robust.
Changes:

  • Use watchdog instead of set_message() and only log when we needed to delete or unpublish, also include the feed URL into the watchdog msg
  • Now it's possible to configure the processor on a per-feed basis so you can decide if the items should be deleted or just unpublished.
  • For delete, instead of node_delete(), use _feedapi_node_node_delete() to avoid perm check, at cron, we're anonymous...
  • Some coding standard improvements
  • Added feedapi_node as a requrement to the .info file

Please review, test and so on.
Known issue: the module totally messes up feed item statistics what is shown after feed refresh (new / updated), it's not trivial to fix.

aron novak’s picture

Title: Can FeedAPI automatically delete items when they are removed from the feed? » FeedAPI GC: handle items what are not in the feed anymore

If some folks review it and confirm that it works, i'm ready to include it in the next release of FeedAPI.

tobey_p’s picture

This Solution sounds nice.
I'll test it for a project i'm working on and will give feedback later on.

rbrownell’s picture

+1 subscribe

aron novak’s picture

Anyone is eager to review #33 before commit?

eusebius’s picture

#33 is just what we needed. It seems to work perfectly for our Google/iCal feed. I hope it will be integrated into the main branch of the module.

Anonymous’s picture

Drupal version: 6.20

#33 appears to unpublish all nodes regardless whether delete or unpublish is selected or the feed item is still in the feed. Have tried different weights.

Edit: No log message is shown after process.

Edit2: On a second attempt. The log message is shown if the feeditems are first manually removed, and then the refresh button is pressed. It shows a list of all the items deleted. Followed by a message how many new nodes were saved. However when you check the new nodes there are none.

Things to check for conflicts:
1. FeedAPI Inherit
2. FeedAPI Mapper

Anonymous’s picture

Version: 6.x-1.9-beta3 » 6.x-1.7-beta2
Debugging

Part 1: Debugging #39 (and possible #26)

Debugging #39 (and possible #26). Using the feedapi_gc.tar_.gz in #21 and feedapi 6.x-1.9-beta3.

The problem might be that 'guid' doesn't appear to exist (in some use cases?):


function feedapi_gc_feedapi_after_refresh($feed) {
  foreach ($feed->items as $item) {
    $present[$item->options->guid] = $item->options->guid;

   // Is there a guid through echo?

  echo $item->options->guid;

 // let's try a var_dump instead

  var_dump($item);

  }

Which would then cause

    if (!isset($present[$feed_item->guid])) {

to always return false.

Hence all the feed node items are getting unpublished/ deleted?
If this is so: What could cause guid to show up in some use cases, and not in others?

Part 2: Possible additional debug of #33

Possible additional debug in feedapi_gc.tar_.gz in #33 tested in feedapi 6.x-1.9-beta3:

1. Feedapi Garbage Collector Module does not save settings in content type and individual feed for gc delete checkbox upon save.
2. Multiple gc delete checkboxes show up in content type (need to double check individual feed if same occurs there).

Anonymous’s picture

StatusFileSize
new1.5 KB

Edit:

Scratch this attachment, may not work.

Anonymous’s picture

Version: 6.x-1.7-beta2 » 6.x-1.9-beta3
StatusFileSize
new1.59 KB

The problem is that some feeds use a term such as 'original_url' instead of 'guid'. Having said that I could not get #21 or #33 to work so far. Cause is yet unknown.

The code attached is a proof of concept and alfa. It should not be considered to be definite code by any means. However it's a code to check if it's even possible to remove old feed item nodes. It does include some documentation.

Basically what it 'should' do when finished:

It looks up the url of new feed items, checks to see if any nodes exist for these based on their nid, and then for the current feed shows the nodes in the database that are not new. (Well that's the idea anyway...)

As I am new with working with the feedapi module, nor a Drupal or PHP master please be gentle. There might be far better ways to go.

Warning: The code is for development purposes only. It should be considered very much alfa, unsecure, and volatile. Do not use on a live website. Use only in a test environment with data that you don't mind losing if things go wrong. It may work, or it may not work.